多线程(一)
目录
一、多线程基础
概述
1.进程与线程
1.1 什么是程序?
1.2 什么是进程?
1.3 什么是线程?
1.4 进程与线程的区别
2.线程基本概念
3.线程的创建与启动
4.线程的创建方式
4.1 方式一:继承java.lang.Thread类(线程子类)
4.2 方式二:实现java.lang.Runnable接口(线程执行类)
4.3 方式三:实现java.util.concurrent.Callable接口,允许子线程返回结果、抛出异常
总结:
4.4 方式四:线程池
5.线程的命名
6.获取线程名
7.线程的休眠(暂停)
8.线程的优先级
9.总结
二、线程的状态及常用方法
1.线程的状态
2.守护线程(Daemon Thread)
2.1 用户线程与守护线程的区别
2.2 设置守护线程
3. 线程的让出:yield()方法
3.1 yield()方法的作用
4.线程的中断:interrupt()方法
4.1 interrupt()方法的作用
4.2 interrupt()方法的原理
5.线程的插队:join()方法
5.1 join()方法的作用
5.2 join()方法的实现原理
4.总结
三、多线程同步
1.多线程的数据不一致
2.synchronized关键字
案例代码
1.多线程售票
四、Synchronized同步锁
1.什么是Synchronized同步锁?
2.什么是锁?
3.synchronized关键字的用法
synchronized修饰实例方法
synchronized修饰静态方法
synchronized修饰代码块
synchronized 关键字的补充
一、多线程基础
概述
现代操作系统(Windows
,macOS
,Linux
)都可以执行多任务。多任务就是同时运行多个任务。例如:播放音乐的同时,浏览器可以进行文件下载,同时可以进行QQ消息的收发。
CPU
执行代码都是一条一条顺序执行的,但是,即使是单核CPU
,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU
对多个任务轮流交替执行。
操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001
秒,让QQ执行0.001
秒,再让音乐播放器执行0.001
秒。在用户使用的体验看来,CPU
就是在同时执行多个任务。
1.进程与线程
1.1 什么是程序?
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,可以理解为程序是包含静态代码的文件。例如:浏览器软件、音乐播放器软件等软件的安装目录和文件。
1.2 什么是进程?
进程是程序的一次执行过程,是系统运行程序的基本单位。在Windows
系统中,每一个正在执行的exe
文件或后台服务,都是一个进程,由操作系统统一管理并分配资源,因此进程是动态的。 例如:正在运行中的浏览器就是一个进程,正在运行中的音乐播放器是另一个进程,同理,正在运行中的QQ
和WPS
等都是进程。
操作系统运行一个程序,即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着, 同时,每个进程还占有某些系统资源如 CPU
时间,内存空间,文件,输入输出设备的使用权等。
1.3 什么是线程?
某些进程内部还需要同时执行多个子任务。例如,我们在使用WPS
时,WPS
可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行自动保存和上传云文档,我们把子任务称为线程。线程是进程划分成的更小的运行单位。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个主线程。
┌──────────┐│Process ││┌────────┐│┌──────────┐││ Thread ││┌──────────┐│Process ││└────────┘││Process ││┌────────┐││┌────────┐││┌────────┐│
┌──────────┐││ Thread ││││ Thread ││││ Thread ││
│Process ││└────────┘││└────────┘││└────────┘│
│┌────────┐││┌────────┐││┌────────┐││┌────────┐│
││ Thread ││││ Thread ││││ Thread ││││ Thread ││
│└────────┘││└────────┘││└────────┘││└────────┘│
└──────────┘└──────────┘└──────────┘└──────────┘
┌──────────────────────────────────────────────┐
│ Operating System │
└──────────────────────────────────────────────┘
例如:我们启动JVM
运行一个Java
程序,其实就是启动了一个 JVM
的进程。在JVM
的进程中,又包含了main 主线程、Reference Handler 清理线程、Finalizer线程(用于调用对象 的finalize()
方法)等线程。
线程是一个比进程更小的执行单位(CPU的最小执行单位)。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。
1.4 进程与线程的区别
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销:每个进程都有独立的代码副本和数据空间,进程之间的切换,资源开销较大;线程可以看做轻量级的进程,每个线程都有自己独立的运行栈和程序计数器,线程之间切换,资源开销小。
包含关系:一个进程内包含有多个线程,在执行过程,线程的执行不是线性串行的,而是多条线程并行共同完成。
内存分配:同一进程内的所有线程共享本进程的内存空间和资源;进程之间的内存空间和资源相互独立。
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响;一个线程崩溃,会导致整个进程退出,所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口和程序出口。但是线程不能独立执行,必须依存在应用程序(进程)中,由应用程序提供多个线程执行控制。
2.线程基本概念
单线程:单线程就是进程中只有一个线程,单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。
public class SingleThread {public static void main(String[] args) {for (int i = 0; i < 10000; i++) {System.out.print(i + " ");}}
}
多线程:由一个以上的线程组成的程序称为多线程程序。Java中,一定是从主线程开始执行(main方法),然后在主线程的某个位置创建并启动新的线程。
public class MultiThread {public static void main(String[] args) {// 创建2个线程Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {System.out.println("线程1:" + i + " ");}}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {System.out.println("线程2:" + i + " ");}}});// 启动2个线程t1.start();t2.start();}
}
3.线程的创建与启动
通过创建Thread
实例,完成线程的创建。
线程的内部实现可以通过继承Thread
类、实现Runnable
接口等方式进行封装。
通过调用Thread
实例的start()
方法启动新线程。
查看Thread
类的源代码,会看到start()
方法内部调用了一个private native void start0()
方法,native
修饰符表示这个方法是由JVM
虚拟机内部的C
代码实现的本地方法,由JVM
根据当前操作系统进行本地实现。
Step* 程序:程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,可以理解为程序是包含静态代码块的文件 * 多进程:进程是程序的一次执行过程,是系统运行程序的基本单位 * 多线程:线程是操作系统运行时能够调度的最小单位,它被包含在进程中,是进程在运行过程中的一个单位 * * 多线程的应用场景: * 软件中的耗时操作,拷贝和迁移文件,加载大量的资源的时候 * 所有的后台服务器 * 所有的聊天软件
public class Main {public static void main(String[] args) {// Step1: main主线程执行输出System.out.println("main start...");// Step2: main主线程,创建子线程sub,输出字母A-ZThread sub = new Thread() {// Step4:子线程被执行时,自动调用run()方法public void run() {for(char c='A';c<='Z';c++){System.out.println("子线程:" + c);}}};// Step3: main主线程,启动子线程subsub.start();// Step4: main主线程执行输出字母的ASCII码for(int c ='a';c<='z';c++){System.out.println("main线程:" + c);}System.out.println("main end...");}
}
Step1:main主线程执行输出
Step2: main
主线程,创建子线程
Step3: main
主线程,启动子线程
Step4: main
主线程与子线程同时运行,由操作系统调度,程序本身无法确定线程的调度顺序
同时,main
主线程执行输出
同时,子线程被执行时,自动调用run()
方法
注意:直接调用Thread
实例的run()
方法是无效的,因为直接调用run()
方法,相当于调用了一个普通的Java
方法,当前线程并没有任何改变,也不会启动新线程。
public class Main {public static void main(String[] args) {Thread t = new MyThread();t.run();System.out.println("再执行main主线程");}
}class MyThread extends Thread {public void run() {System.out.println("先执行子线程");}
}
运行结果:
先执行子线程
再执行main主线程
4.线程的创建方式
4.1 方式一:继承java.lang.Thread类(线程子类)
public class Demo01 {public static void main(String[] args) {//方式一//1.创建一个Thread的子类,重写run方法//2.创建子类对象//3.调用start方法启动MyTread t1=new MyTread();t1.start();for (int i = 0; i <100 ; i++) {System.out.println("main线程执行任务:"+i);}}
}
class MyTread extends Thread{@Overridepublic void run() {for (int i = 0; i <100 ; i++) {System.out.println(getName()+"执行任务:"+i);}}
}
4.2 方式二:实现java.lang.Runnable接口(线程执行类)
public class Demo02 {public static void main(String[] args) {//方式二:java.lang.Runnable 接口实现多线程//1.创建Runnable的实现类,重写run方法//2.创建Runnable的实现类对象r1//3.创建Thread类的对象,将r1作为构造方法的参数进行传递//4.调用start方法启动线程MyRun r1=new MyRun();Thread t1=new Thread(r1);t1.start();for (int i = 0; i <100 ; i++) {System.out.println("main线程执行任务:"+i);}}
}
class MyRun implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName()+"执行任务:"+i);}}
}
4.3 方式三:实现java.util.concurrent.Callable接口,允许子线程返回结果、抛出异常
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo03 {public static void main(String[] args) throws ExecutionException, InterruptedException {//方式三:Callable接口方式实现多线程//1.创建Callable的实现类,重写call方法//2.创建Callable的实现类对象//3.创建FutureTask对象,用来进行结果的管理操作//4.创建Thread类的对象,将步骤3的对象作为参数传递//5.启动线程MyCall c1=new MyCall(1,100);FutureTask<Integer> ft=new FutureTask<>(c1);Thread t1=new Thread(ft);t1.start();System.out.println("子线程c1的结果为:"+ft.get());MyCall c2=new MyCall(1,10);FutureTask<Integer> ft2=new FutureTask<>(c2);Thread t2=new Thread(ft2);t2.start();System.out.println("子线程c2的结果为:"+ft2.get());}
}
class MyCall implements Callable<Integer>{int begin,end;public MyCall(int begin,int end){this.begin=begin;this.end=end;}@Overridepublic Integer call() throws Exception {int number=0;for (int i = begin; i <=end; i++) {number+=i;}return number;}
}
总结:
* Thread 编程比较简单,可以直接使用 Thread 类中的方法 可拓展性差 * Runnable 扩展性强,实现该接口后还可以继承其他的类再实现其他的接口。 * Callable 扩展性强,实现该接口还可以继承其他类,可以有返回值
4.4 方式四:线程池
线程池,按照配置参数(核心线程数、最大线程数等)创建并管理若干线程对象。程序中如果需要使用线程,将一个执行任务传给线程池,线程池就会使用一个空闲状态的线程来执行这个任务。执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。使用线程池可以很好地提高性能。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo04 {public static void main(String[] args) {//使用线程池创建线程对象ExecutorService es= Executors.newCachedThreadPool();es.execute(new MyRun());es.execute(new MyRun());}
}
5.线程的命名
生产环境中,为了排查问题方便,建议在创建线程的时候指定一个合理的线程名字。
调用父类的setName()方法或在构造方法中给线程名字赋值。
如果没有为线程命名,系统会默认指定线程名,命名规则是Thread-N的形式。
6.获取线程名
调用父类的getName()方式获取线程名,如果我们没有给线程设置名字,线程默认名称是Thread-X,X使用从0开始。
7.线程的休眠(暂停)
在线程中,可以通过调用Thread.sleep(long millis),强迫当前线程按照指定毫秒值休眠。
public class Demo01 {public static void main(String[] args) throws InterruptedException {//给线程设置名字//setName()方式设置线程名//调用有参构造方法设置线程名//获取线程名//getName()方式获取线程名,如果我们没有给线程设置名字,线程默认名称是Thread-X,X使用从0开始//获取当前线程:Thread.currentThread()//让当前线程休眠:Thread.sleep(毫秒数)MyThread t1=new MyThread();t1.setName("土豆");
// Thread.sleep(1000);t1.start();//使用构造方法设置线程名MyThread t2=new MyThread("洋芋");
// Thread.sleep(1000);t2.start();//Runnable作为参数Runnable r1=new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"线程正在启动");try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+"线程运行结束");}};Thread t3=new Thread(r1,"马铃薯");t3.start();}
}
public class MyThread extends Thread{public MyThread() {}public MyThread(String name) {super(name);}@Overridepublic void run() {for (int i = 0; i <100 ; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("当前线程是:"+getName()+i);}}
}
8.线程的优先级
在线程中,通过setPriority(int n)设置线程优先级,范围是1-10,默认为5。
优先级高的线程被操作系统调度的优先级较高(操作系统对高优先级线程,调度更频繁)
注意:并不能代表,通过设置优先级来确保优先级高的线程一定会先执行。
public class Demo02 {public static void main(String[] args) {//创建线程1Thread t1=new Thread(new Runnable() {@Overridepublic void run() {for (char i = 'a'; i <'z' ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}},"线程1");t1.setPriority(1);//创建线程2Thread t2=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <26 ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}},"线程2");t2.setPriority(10);//优先级越高、抢占到资源的概率越大t1.start();t2.start();//获取线程优先级System.out.println(t1.getName()+"优先级为"+t1.getPriority());System.out.println(Thread.currentThread().getName()+"优先级为"+Thread.currentThread().getPriority());}
}
9.总结
(1)Java用Thread对象表示一个进程,通过调用start()启动一个新线程。
(2)一个线程对象只能调用一次start()方法。
(3)线程的执行代码写在run()方法中。
(4)线程调度由操作系统决定,程序本身无法决定调度顺序。
(5)Thread.sleep()可以把当前线程暂停一段时间。
二、线程的状态及常用方法
1.线程的状态
(1)在Java
程序中,一个线程对象通过调用start()
方法启动线程,并且在线程获取CPU
时,自动执行run()
方法。run()
方法执行完毕,代表线程的生命周期结束。
(2)在整个线程的生命周期中,线程的状态有以下6
种:
New
:新建状态,新创建的线程,此时尚未调用start()
方法;
Runnable
:运行状态,运行中的线程,已经调用start()
方法,线程正在或即将执行run()
方法;
Blocked
:阻塞状态,运行中的线程,在等待竞争锁时,被阻塞,暂不执行;
Waiting
:等待状态,运行中的线程,因为join()
等方法调用,进入等待;
Timed Waiting
:计时等待状态,运行中的线程,因为执行sleep(等待毫秒值)
join(等待毫秒值)
等方法,进入计时等待;
Terminated
:终止状态,线程已终止,因为run()
方法执行完毕。
当线程启动后,它可以在Runnable
、Blocked
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。
线程终止的原因有:
线程正常终止:run()
方法执行到return
语句返回;
线程意外终止:run()
方法因为未捕获的异常导致线程终止;
对某个线程的Thread
实例调用stop()
方法强制终止(宇宙超级无敌强烈不推荐);
2.守护线程(Daemon Thread)
2.1 用户线程与守护线程的区别
用户线程:我们平常创建的普通线程。
守护线程:用来服务于用户线程的线程,在JVM
中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出;而守护线程执行结束后,虚拟机不会自动退出。
2.2 设置守护线程
在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程。
public class Demo03 {public static void main(String[] args) {MyThread t1=new MyThread("女神"){@Overridepublic void run() {for (char i = 'a'; i <='z' ; i++) {System.out.println(getName()+":"+i);}}};MyThread t2=new MyThread("备胎"){@Overridepublic void run() {for (int i = 0; i <=100 ; i++) {System.out.println(getName()+":"+i);}}};//将t2设置为守护线程//细节:当其他的非守护线程执行完毕后,守护线程会陆陆续续结束t2.setDaemon(true);t1.start();t2.start();}
}
3. 线程的让出:yield()方法
3.1 yield()方法的作用
线程通过调用yield()
方法告诉JVM
的线程调度,当前线程愿意让出CPU
给其他线程使用。
至于系统是否采纳,取决于JVM
的线程调度模型:分时调度模型和抢占式调度模型。
分时调度模型:所有的线程轮流获得 cpu
的使用权,并且平均分配每个线程占用的 CPU
时间片;
抢占式调度模型:优先让可运行池中优先级高的线程占用 CPU
,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU
。(JVM
虚拟机采用的是抢占式调度模型 )。
/***yield()方法源代码* A hint to the scheduler that the current thread is willing to yield* its current use of a processor. The scheduler is free to ignore this* hint.* 向调度程序发出的提示,表示当前线程愿意让出处理器的使用权*/public static native void yield();
案例:通过yield()让出CPU。
public class Demo04 {public static void main(String[] args) {//创建线程1Thread t1=new Thread(new Runnable() {@Overridepublic void run() {for (char i = 'a'; i <'z' ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);//线程的让出Thread.yield();}}},"线程1");t1.setPriority(1);//创建线程2Thread t2=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <26 ; i++) {System.out.println(Thread.currentThread().getName()+":"+i);Thread.yield();}}},"线程2");}
}
4.线程的中断:interrupt()方法
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()
方法,使得自身线程能立刻结束运行。
例如:假设从网络下载一个100
M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()
方法,目标线程需要反复检测自身状态是否是interrupted
状态,如果是,就立刻结束运行。
4.1 interrupt()方法的作用
interrupt()
方法的作用是设置该线程的中断状态为true
,线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于中断状态。线程会不时地检测这个中断状态值,以判断线程是否应该被中断(中断状态值是否为true
)。
4.2 interrupt()方法的原理
interrupt()
方法只是改变中断状态,不会像stop()
中断一个正在运行的线程。支持线程中断的方法(Thread.sleep()
、join()
、wait()
等方法)就是在监视线程的中断状态,一旦发现线程的中断状态值被置为“true
”,就会抛出线程中断的异常InterruptedException
,给WAITING
或者TIMED_WAITING
等待状态的线程发出一个中断信号,线程检查中断标识,就会以退出WAITING
或者TIMED_WAITING
等待状态;
注意事项:
线程被Object.wait()
, Thread.join()
和Thread.sleep()
三种方法阻塞或等待,此时调用该线程的interrupt()
方法,那么该线程将抛出一个 InterruptedException
中断异常,从而提前终结被阻塞状态。
如果线程没有被阻塞或等待,调用 interrupt()
将不起作用,直到执行到wait()
,sleep()
,join()
等方法进入阻塞或等待时,才会抛出 InterruptedException
异常;
public class Demo06 {public static void main(String[] args) throws InterruptedException {System.out.println("main线程进入");Thread t1=new Thread(new Runnable() {@Overridepublic void run() {//获取当前的系统时间long startTime=System.currentTimeMillis();System.out.println("进入t1线程");try {Thread.sleep(1000*10);} catch (InterruptedException e) {System.out.println("中断t1线程,消耗时间为:"+(System.currentTimeMillis()-startTime));e.printStackTrace();return;}System.out.println("结束t1线程,消耗时间为:"+(System.currentTimeMillis()-startTime));}},"线程1");t1.start();//让主线程休眠Thread.sleep(1000*3);System.out.println("main线程结束");//main主线程修改t1线程的中断默认状态false变为true//t1线程检测到中断状态=true,就会抛出InterruptedException异常,子线程执行结束t1.interrupt();}
}
5.线程的插队:join()方法
5.1 join()方法的作用
t.join()
方法会使当前线程( 主线程 或者调用t.join()
的线程 )进入等待池,并等待 线程t 执行完毕后才会被唤醒。此时,并不影响同一时刻处在运行状态的其他线程。
//线程的插队
public class Demo05 {public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(){@Overridepublic void run() {System.out.println("进到"+Thread.currentThread().getName()+"中");for (int i = 0; i <=100 ; i++) {System.out.println(i);}System.out.println("结束"+Thread.currentThread().getName());}};t1.start();//如果想要线程1先执行,线程2后执行,可以使用线程插队//线程的插队,插入当前的线程的前面t1.join();//主线程执行的任务for (char i = 'a'; i <='z' ; i++) {System.out.println("main:"+i);}}
}
5.2 join()方法的实现原理
(1)join()
方法的底层是利用wait()
方法实现;
(2)join()
方法是一个synchronized
同步方法,当主线程调用 线程t.join( )方法时,主线程先获得了 线程t对象 的锁,随后进入join()
方法,调用 线程t对象 的wait()
方法,使主线程进入了 线程t对象 的等待池;
(3)等到 线程t 执行完毕之后,线程在TERMINATED
终止状态的时候会自动调用自身的notifyAll()
方法,来唤醒所有处于等待状态的线程:这个机制在隐藏在native
本地方法中,由一个C++
实现的方法ensure_join()
函数实现。在该函数的尾部,执行了lock.notify_all(thread);
,相当于调用了notifyAll()
方法。
//ensure_join( )函数源代码
static void ensure_join(JavaThread*thread) {Handle threadObj(thread, thread -> threadObj());ObjectLocker lock(threadObj, thread);thread -> clear_pending_exception();java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);java_lang_Thread::set_thread(threadObj(), NULL);//下行执行了notifyAll()操作lock.notify_all(thread);thread -> clear_pending_exception();
}
综上所述:join()
方法实际上是通过调用wait()
方法, 来实现同步的效果的。
例如:A线程中调用了B线程的join()
方法,则相当于A线程调用了B线程的wait()
方法,在调用了B线程的wait()
方法后,A线程就会进入WAITING
或者TIMED_WAITING
等待状态,因为它相当于放弃了CPU
的使用权。
注意:join(0)
的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕:即join(0)
=join()
;
//join( )方法源代码
public class Thread implements Runnablepublic final void join() throws InterruptedException {join(0);}public final synchronized void join(long millis) throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) {// 无限等待wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}// 计时等待wait(delay);now = System.currentTimeMillis() - base;}}}
}
4.总结
(1)线程的状态有以下6
种:New
、Runnable
、Blocked
、Waiting
、Timed Waiting
、Terminated
。
(2)join()
方法用于实现线程插队,调用完毕后会释放锁。
(3)sleep()
方法用于实现线程休眠,调用完毕后不会释放锁。
(4)interrupt()
方法用于设置该线程的中断状态为true
。
(5)yield()
方法用于让出CPU
执行。
(6)JVM
的线程调度模型:分时调度模型和抢占式调度模型。
三、多线程同步
1.多线程的数据不一致
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
这个时候,一个在单线程模型下不存在的问题就会发生:如果多个线程同时读写共享变量,会出现数据不一致的问题。例如:
public class Main {public static void main(String[] args) throws Exception {Thread add = new AddThread();Thread dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);}
}class Counter {public static int count = 0;
}class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) { Counter.count += 1; }}
}class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) { Counter.count -= 1; }}
}
在上述代码案例中,两个线程同时对一个int
变量进行操作,一个加10000
次,一个减10000
次,最后结果应该是0
,但是,每次运行,结果实际上都是不一样的。
这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
例如:n = n + 1;
看上去是一行语句,实际上对应了3
条指令:
ILOAD
IADD
ISTORE
我们假设n
的值是100
,如果两个线程同时执行n = n + 1
,得到的结果很可能不是102
,而是101
,原因在于:
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘│ ││ILOAD (100) ││ │ILOAD (100)│ │IADD│ │ISTORE (101)│IADD ││ISTORE (101)│▼ ▼
如果线程1在执行ILOAD
后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD
后获取的值仍然是100
,最终结果被两个线程的ISTORE
写入后变成了101
,而不是期待的102
。
说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘│ ││-- lock -- ││ILOAD (100) ││IADD ││ISTORE (101) ││-- unlock -- ││ │-- lock --│ │ILOAD (101)│ │IADD│ │ISTORE (102)│ │-- unlock --▼ ▼
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
2.synchronized关键字
综上所述,保证一段代码的原子性就是通过加锁和解锁实现的。在Java
的多线程模型中使用synchronized
关键字对一个对象进行加锁:
synchronized(lock) {n = n + 1;
}
synchronized
保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized
改写如下:
//多线程数据不一致
public class Main {public static void main(String[] args) throws Exception {Thread add = new AddThread();Thread dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);}
}class Counter {public static final Object lock = new Object();public static int count = 0;
}class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.count += 1;}}}
}class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.count -= 1;}}}
}
注意下列代码:它表示用Counter.lock
实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。这样一来,对Counter.count
变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0
。
synchronized(Counter.lock) { // 获取锁...
} // 释放锁
使用synchronized
解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized
代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized
会降低程序的执行效率。
概括总结一下如何使用synchronized
:
1.找出修改共享变量的线程代码块;
2.选择一个共享实例作为锁;
3.使用synchronized(lockObject) { ... }
。
注意事项:
抛出异常:在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:
public void add(int m) {synchronized (obj) {if (m < 0) {throw new RuntimeException();}this.value += m;} // 无论有无异常,都会在此释放锁
}
不同的lock:下列代码是一个错误使用synchronized
的例子:
public class Main {public static void main(String[] args) throws Exception {Thread add = new AddThread();Thread dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);}
}class Counter {public static final Object lock1 = new Object();public static final Object lock2 = new Object();public static int count = 0;
}class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock1) {Counter.count += 1;}}}
}class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock2) {Counter.count -= 1;}}}
}
结果并不是0
,这是因为两个线程各自的synchronized
锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM
只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。
因此,使用synchronized
的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。
案例代码
1.多线程售票
3
个线程模拟3
个售票窗口,卖出公共票池的20
张票。
方式 1 : 使用 synchronized 实现实现方式中,包含有一个常见错误
//启动类
public class Main {public static void main(String[] args) {// 公共票池,默认门票数量为20张TicketPool publicTicketPool = new TicketPool(20);// 创建3个线程,用于模拟3个不同的售票窗口,共同卖出公共票池中的20张门票new Thread(publicTicketPool, "窗口1").start();new Thread(publicTicketPool, "窗口2").start();new Thread(publicTicketPool, "窗口3").start();}
}
//公共票池类
public class TicketPool implements Runnable {// 当前剩余门票数private int ticketNum;// 创建公共票池时,传入默认总门票数public TicketPool(int ticketNum) {this.ticketNum = ticketNum;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "准备开始卖票");// 线程竞争CPU执行权(this锁)synchronized (this) {while(true) {if (ticketNum <= 0) {System.out.println(Thread.currentThread().getName() + "没有票了");return;} else {System.out.println(Thread.currentThread().getName() + "卖出了一张门票,剩余" + --ticketNum + "张门票");try {// 当前线程休眠1000毫秒,模拟延迟// 注意:休眠过程中,当前线程不会让出持有的"this锁",此处为引发错误的原因Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}}public int getTicketNum() {return ticketNum;}public void setTicketNum(int ticketNum) {this.ticketNum = ticketNum;}
}
运行结果:错误仅有一个窗口(线程)参与售票,其它窗口(线程)闲置
窗口2准备开始卖票
窗口3准备开始卖票
窗口1准备开始卖票
窗口2卖出了一张票,剩余19张票
窗口2卖出了一张票,剩余18张票
窗口2卖出了一张票,剩余17张票
窗口2卖出了一张票,剩余16张票
窗口2卖出了一张票,剩余15张票
窗口2卖出了一张票,剩余14张票
... ...
代码修正
// 当前线程休眠1000毫秒,模拟延迟
// 注意:休眠过程中,当前线程不会让出持有的"this锁"
// Thread.sleep(1000);// 持有“this锁”的当前线程等待1000毫秒,模拟延迟
// 注意:等待过程中,当前线程让出持有的"this锁",允许其它线程参与竞争CPU执行权(this锁)
this.wait(1000);
方式 2 : 使用公共Object对象锁多个线程持有同一个Object对象锁
//启动类
public class Main {public static void main(String[] args) {// 公共对象锁Object lockObj = new Object();// 公共票池,默认门票数量为20张TicketPool publicTicketPool = new TicketPool(20,lockObj);// 创建3个线程,用于模拟3个不同的售票窗口,共同卖出公共票池中的20张门票new Thread(publicTicketPool, "窗口1").start();new Thread(publicTicketPool, "窗口2").start();new Thread(publicTicketPool, "窗口3").start();}
}
//公共票池类
public class TicketPool implements Runnable {// 当前剩余票数private int ticketNum;// 对象锁private Object lockObj;// 创建线程时,传入总票数 + 对象锁public TicketPool(int ticketNum,Object lockObj) {this.ticketNum = ticketNum;this.lockObj = lockObj;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "准备开始卖票");// 线程竞争CPU执行权(this锁)synchronized (lockObj) {while(true) {if (ticketNum <= 0) {System.out.println(Thread.currentThread().getName() + "没有票了");return;} else {System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余" + --ticketNum + "张票");try {// 持有“lockObj锁”的当前线程等待1000毫秒,模拟延迟// 注意:等待过程中,当前线程让出持有的"lockObj锁"lockObj.wait(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}}public int getTicketNum() {return ticketNum;}public void setTicketNum(int ticketNum) {this.ticketNum = ticketNum;}
}
运行结果:正常所有窗口均参与卖票,剩余票量正常
窗口1准备开始卖票
窗口3准备开始卖票
窗口2准备开始卖票
窗口1卖出了一张票,剩余19张票
窗口2卖出了一张票,剩余18张票
窗口3卖出了一张票,剩余17张票
窗口3卖出了一张票,剩余16张票
窗口2卖出了一张票,剩余15张票
窗口1卖出了一张票,剩余14张票
窗口1卖出了一张票,剩余13张票
窗口2卖出了一张票,剩余12张票
窗口3卖出了一张票,剩余11张票
窗口2卖出了一张票,剩余10张票
窗口3卖出了一张票,剩余9张票
窗口1卖出了一张票,剩余8张票
窗口3卖出了一张票,剩余7张票
窗口2卖出了一张票,剩余6张票
窗口1卖出了一张票,剩余5张票
窗口3卖出了一张票,剩余4张票
窗口2卖出了一张票,剩余3张票
窗口1卖出了一张票,剩余2张票
窗口3卖出了一张票,剩余1张票
窗口1卖出了一张票,剩余0张票
窗口2没有票了
窗口1没有票了
窗口3没有票了
四、Synchronized同步锁
1.什么是Synchronized同步锁?
Synchronized
同步锁,简单来说,使用Synchronized
关键字将一段代码逻辑,用一把锁给锁起来,只有获得了这把锁的线程才访问。并且同一时刻, 只有一个线程能持有这把锁, 这样就保证了同一时刻只有一个线程能执行被锁住的代码,从而确保代码的线程安全。
2.什么是锁?
每个Java
对象都可以充当一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock
)或者监视器锁(Monitor Lock
)。
synchronized(reference-to-lock) {//临界区
}
其中, 括号里面的reference-to-lock
就是锁的引用, 任何一个Java
对象都可以成为reference-to-lock
。 你可以实例化一个Object
对象,将它作为锁。如果直接使用this
,代表使用当前对象作为锁。
3.synchronized关键字的用法
(1)修饰实例方法:synchronized
修饰实例方法, 则用到的锁,默认为this
当前方法调用对象;
(2)修饰静态方法:synchronized
修饰静态方法, 则其所用的锁,默认为Class
对象;
(3)修饰代码块:synchronized
修饰代码块, 则其所用的锁,是某个指定Java
对象;
synchronized修饰实例方法
使用当前对象this充当锁,完成对当前方法的锁定,只有获取this
锁的线程才能访问当前方法;
并发过程中,同一时刻,可以有N
个线程请求执行方法,但只有一个线程可以持有this
锁,才能执行;
不同线程,持有的对象,必须相同;
当使用synchronized
修饰实例方法时, 以下两种写法作用和意义相同:
public class Foo {// 实例方法public synchronized void doSth1() {// 获取this锁,才能执行该方法}// 实例方法public void doSth2() {synchronized(this) {// 获取this锁,才能执行该代码块}}
}
public static void main(String[] args) {// 实例化一个对象Foo fa = new Foo();// 创建不同的线程1Thread thread01 = new Thread() {public void run() {// 使用相同的对象访问synchronized方法fa.doSth1();}};// 创建不同的线程2Thread thread02 = new Thread() {public void run() {// 使用相同的对象访问synchronized方法fa.doSth1();}};// 启动线程thread01.start();thread02.start();
}
synchronized修饰静态方法
使用当前对象的Class
对象充当锁,完成对当前方法的锁定,只有获取Class
锁的线程才能访问当前方法;
不同线程,持有的对象,可以不同,但必须相同class
类型;
当使用synchronized
修饰静态方法时, 以下两种写法作用和意义相同:
public class Foo {// 静态方法public synchronized static void doSth1() {// 获取当前对象的Class对象锁,才能执行该方法}// 实例方法public static void doSth2() {synchronized(this.getClass()) {// 获取当前对象的Class对象锁,才能执行该代码块}}
}
public static void main(String[] args) {// 创建不同的对象(相同类型)Foo fa = new Foo();Foo fb = new Foo();// 创建不同线程1Thread thread01 = new Thread() {public void run() {// 使用不同的对象访问synchronized方法fa.doSth2();}};// 创建不同线程2Thread thread02 = new Thread() {public void run() {// 使用不同的对象访问synchronized方法fb.doSth2();}};// 启动线程thread01.start();thread02.start();
}
synchronized修饰代码块
synchronized(自定义对象) {//临界区
}
synchronized 关键字的补充
当一个线程访问对象的一个synchronized(this)
同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)
同步代码块。在没有加锁的情况下, 所有的线程都可以自由地访问对象中的代码, 而synchronized关键字只是限制了线程对于已经加锁的同步代码块的访问,并不会对其他代码做限制。所以,同步代码块应该越短小越好。
父类中synchronized
修饰的方法,如果子类没有重写,则该方法仍然是线程安全性;如果子类重写,并且没有使用synchronized
修饰,则该方法不是线程安全的;
在定义接口方法时,不能使用synchronized
关键字;
构造方法不能使用synchronized
关键字,但可以使用synchronized
代码块来进行同步;
离开synchronized
代码块后,该线程所持有的锁,会自动释放;