引言:线程是为了处理并发任务的,一个线程完成一个任务,写线程实际上就是写任务,其核心点就是拆分任务交给线程去完成,本文只简单介绍Java多线程的入门,线程过于复杂,所以这里只做简单的入门解析。
一,多线程的概念
1.线程和进程
进程:简单理解,进程就是一个程序,运行在内存中。每次需要运行这个程序,都需要把它加进内存中。(可以打开任务管理器)里面所显示的就是进程。每个进程都有独立的空间。
线程:线程是进程里面的一个单元,也可以看作是一个独立运行的程序。比如main方法也是一个线程,我们管他叫主线程。在一个进程里面,可能有多个线程,他们共享同一数据:存于堆中的数据。
多线程设计的目的:优点:多个线程效率快,并且能实现高并发。缺点:多线程存在安全问题,但是为了效率和速度牺牲一部分安全性是可以的,我们可以通过其他手段来尽可能保障安全。
2.并行和并发
并行:取决于硬件,所谓并行就是多任务系统,多个任务同时进行。CPU单核数不能并行。
并发:看似同时发生的事件;例如两人过独木桥,两人一前一后过桥,这就叫并发(独木桥比喻的是CPU核数,意思是并发实际上是CPU单核就可以完成的多任务);而并行则是:两人并排过自己的桥。
3.认识线程
线程就是一个Thread类型的对象。
举例:main方法也是一个线程,为主线程。
public class Test {
public static void main(String[] args) {
//获取当前线程
Thread th = Thread.currentThread();
//更改th线程的名字
th.setName("主线程");
//打印线程th的名字
System.out.println(th.getName());
//打印线程th的编号
System.out.println(th.getId());
//打印线程th的优先级
System.out.println(th.getPriority());
}
}
输出结果为: main(更改名字后为 “主线程”)
1
5
二,创建线程
创建线程的四种方式:
🔴继承Thread类
🔴实现Runnable接口
🔴实现Callable接口
🔴线程池
1.继承Thread类
编写类>>new对象>>调方法
1.1.1编写类
public class MyThread extends Thread{
//在线程里面,完成任务必须要用到run方法
public void run() {
//执行的任务代码块
//执行输出循环0-100的数字,并且带上执行的线程名。
for(int i = 0;i<=100;i++) {
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
}
1.1.2 new对象和调方法。
public class Test {
public static void main(String[] args) {
MyThread mth1 = new MyThread();
// mth1.run(); 此处不能直接调用run方法,这里调用run方法是由main线程来执行的,所以输出结果的前面线程名为mian-1,main-2····
mth1.start();//启动线程,线程调用方法要使用start,这里输出结果为Thread0-1,Thread0-2···
}
}
🔴注意:start调的run方法没有参数,并且不能抛异常。传参时,请注意run方法是否满足重写。
2.实现Runnable接口
2.1编写类
public class MyThread implements Runnable{ //实现Runnable接口
//在线程里面,完成任务必须要用到run方法
public void run() {
//执行的任务代码块
for(int i = 0;i<=100;i++) {
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
}
2.2 new对象和调方法
public class Test {
public static void main(String[] args) {
MyThread mth1 = new MyThread();
//Thread类能调到start方法
Thread th1 = new Thread(mth1);
th1.start();
}
}
🔴注意:实现Runnable接口时,不能直接使用new的对象调用start方法;原因是因为Runnable中没有start方法;我们需要将实现了Runnable接口的对象参数传给Thread的对象,这样才能用Thread的对象调用start方法。
实现接口的方式要好一些,为常用方法;原因:
1,java是单继承,如果我们的类已经继承了一个类,就没法继承线程类了。
2,我们实现接口可以方便共享数据。
🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻
例如:两个线程共同输出100个数字🔻
第一步,编写类
public class MyThread implements Runnable {
int num = 100;
public void run() {
while (true) {
if (num <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + ":" + num--);
}
}
}
第二步,new对象以及调用方法。
public class Test {
public static void main(String[] args) {
//设计一个共享对象
MyThread mth1 = new MyThread();
Thread th1 = new Thread(mth1);
Thread th2 = new Thread(mth1);
th1.start();
th2.start();
}
}
🔴注意:两个线程共享run方法,但是会生成两个单独的方法栈,相当于每个线程都有自己的方法栈去运行。
线程的使用以及生命周期:
编写模板>>创建线程对象(新建)>>就绪,运行>>阻塞>>就绪,运行>>死亡(run方法执行完毕)。
3. 实现Callable接口
如果我计算完成能够把结果返回给你,那我们就可以直接在调用者那获取线程的结果,
🔴如果调用线程需要线程返回结果,就用Callable 。
优点:1,有返回值。 2,可以抛异常。(而Runnable不可抛异常,只能try-catch)
import java.util.concurrent.Callable;
public class Test3 implements Callable {
int[] nums;
public Test3(int[] nums){
this.nums = nums;
}
@Override
//这里的call方法自动阻塞,方法执行完后才能拿到它返回的结果
public Integer call() throws Exception {
int sum = 0;
for(int i = 0;i<nums.length;i++){
sum += nums[i];
}
return sum;
}
}
三,线程的调度
线程调度就是线程的几个方法:
🔻setPriority()设置线程的优先级 1-10(默认是5)。
设置了线程的优先级,仅仅是这个线程有概率影响这个线程的运行,不是绝对的。
🔻setName()更改线程的名字。
🔻Thread.sleep()静态方法 线程休眠。
休眠要释放CPU,但是不释放锁。
🔻Thread.yield() 礼让方法,让该线程暂时释放CPU,但该线程依旧可以抢CPU资源。
🔻join 线程加入方法。
应用场景:两个线程并行,在某个时间点,一个线程绝对优先执行(谁调用谁优先),等该线程执行完毕后,另外一个线程才能执行。
会阻塞当前运行线程,不影响其余线程。
1.线程中断
中断其实就是一个标记,中断这个方法是线程类里面的,我们在run里面去监测这个中断,意思是在run里面去调用这个中断方法,所以我们使用继承来测试。
package demo;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
InterThread th = new InterThread();
th.start();
//中断
System.out.println("是否中断线程?y/n");
Scanner input = new Scanner(System.in);
char choice = input.next().charAt(0);
if(choice == 'y') {
th.interrupt();//中断
}
}
}
class InterThread extends Thread{
public void run() {
while(true) {
//检测是否收到中断信号
if(this.isInterrupted()) {
System.out.println("线程被中断");
break;
}
}
}
}
运行结果:
是否中断线程?y/n
y
线程被中断
🔴注意:线程点出来的interrupt方法只是将原线程中的一个boolean值从false变为true,只是一个信号,并不会真正中断线程。如果要实现线程的中断,则需要去检测有没有收到这个信号(这个boolean值是否变为true),之后自己编写中断代码。(也可以在接受到信号时,做一些其他的事情,不一定是中断,要灵活运用)sleep时中断用catch异常捕获,直接中断sleep。
2.守护线程
守护线程就是在后台运行的线程,当所有的用户线程执行完后,它才会结束。
自己设置守护线程的方法: ThreadName.setDaemon(true);
四,线程安全
1.synchonized简单使用
隐式实现锁,正因为是隐式实现锁,所以才很难看清楚原理是怎么实现的。
其原理下文会讲到。
同步代码块:
package site;
//举例,卖票网站的实现原理
//举例为网站只有一个,大家买票都在这里
//所以这里我们实现Runnable接口来达成数据共享
public class Site implements Runnable{
int num = 10;
int count = 0;
//卖票的方法
@Override
public void run() {
System.out.println(this.hashCode());
while (true){
//以下的this指的是Site对象,因为例子只有一个对象,所以可以用this来锁
synchronized (this){//同步代码块
if (num<=0){
break;
}
count++;
num--;
}
System.out.println(Thread.currentThread().getName()+"买到了第几"+count+"张票,还剩下"+num+"张票");
}
}
}
Test:
package site;
public class Test {
public static void main(String[] args) {
Site site = new Site();
System.out.println(site.hashCode());
Thread th1 = new Thread(site,"淘票票");
Thread th2 = new Thread(site,"去哪儿");
Thread th3 = new Thread(site,"携程");
th1.start();
th2.start();
th3.start();
}
}
🔴此处举例是因为new出的三个线程需要共用一个对象,所以我们只需要锁住三个线程的访问点,也就是this,此处的this就是new出的site对象。通过hashcode值也可以证明此点。可以理解为,现在this就是锁,三个线程当中每一个线程要想执行run方法,都需要持有这把锁,而这把锁也只能同一时间被一个线程持有。这样就避免了三个线程同时访问数据,更改数据的时候产生的线程安全。
同步方法块:
package site;
public class SiteMethod implements Runnable {
int num = 10;
int count = 0;
boolean flag = true;
@Override
public void run() {
while (flag) {
sale();
}
}
//同步方法,当一个线程执行该方法时,其余线程不能执行
public synchronized void sale() {
if (num <= 0) {
flag = false;
return;//结束
}
count++;
num--;
System.out.println(Thread.currentThread().getName() + "买到了第几" + count + "张票,还剩下" + num + "张票");
}
}
🔴同步方法后的锁为当前对象this,同步方法不能写对象,只能用当前对象做锁。如果方法为静态方法,则不能用synchronized(对象锁)加锁。
synchronized底层原理:
synchronized在实现时,并不是在编译时加几行代码,而是在编译后的字节码(class)文件中添加了几行指令(monitorenter)(monitorexit)>>监视器,这就是synchronized底层的实现。jdk1.6之前synchronized是通过互斥来实现的,这会导致效率很低。1.6之后有了 🔻锁的升级膨胀🔻 。