java多线程从入门到精通看这篇就够了
目录
- 一.认识线程及线程的创建
- 1.线程的概念
- 2.线程的特性
- 3.线程的创建方式
- <1>继承Thread类
- <2>实现Runnable接口
- <3>实现Callable接口
- 二.线程的常用方法
- 1.构造方法和属性的获取方法
- 2.常用方法
- <1>run()和start()
- <2>interrupt()方法
- <3>join方法
- <4>获取当前线程的引用currentThread();方法
- <5>休眠当前线程sleep();方法
- <6>线程让步yield();方法
- 三.线程的生命周期和状态转换
- 四.线程间的通信
- 五.多线程的安全及解决
- 1.原子性
- 2.可见性
- 3.代码的顺序性
- 4.线程不安全问题的解决
- <1>synchronized 关键字
- <2>volatile 关键字
- 六.锁体系
- 1.Synchronized加锁方式
- <1>Synchronized的加锁方式及语法基础
- <2>Synchronized的原理及实现
- <3>JVM对Synchronized的优化
- 2.常见的锁策略及CAS
- <1>.乐观锁和悲观锁
- <2>自旋锁
- <3>可重入锁
- 3.Lock体系
- <1>Lock接口
- <2>AQS简单认识
- <3>ReentrantLock
- <4>ReadWriteLock锁
- 4.Lock锁和同步锁(synchronized)的区别
- 5.死锁
- 七.多线程案例
- 1.生产者消费者问题
- 2.单例模式
- 3.阻塞式队列
- 4.线程池
- 八.总结
一.认识线程及线程的创建
1.线程的概念
线程和进程的区别:
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
一个进程内的线程之间是可以共享资源的。
每个进程至少有一个线程存在,即主线程。
注:
每个进程至少有一个线程存在,即主线程(系统级别的,C语言的主线程)
java级别的主线程(自己写的入口函数main方法(可以没有这个线程)
对java进程来说,至少有一个非守护线程还没终止,进程就不会结束
2.线程的特性
在后面线程的安全性会详细介绍
1.原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
2.可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.有序性:程序执行的顺序按照代码的先后顺序执行。
3.线程的创建方式
<1>继承Thread类
class MyThread extends Thread{ @Override public void run() { System.out.println("继承Thread类创建线程"); } } public static void main(String[] args) { //1.继承Thread类创建线程 MyThread t=new MyThread(); t.start(); }
<2>实现Runnable接口
1.将MyRunnable对象作为任务传入Thread中
class MyRunnable implements Runnable{ @Override public void run() { System.out.println("继承Runnable接口,创建描述任务对象,实现多线程"); } } public static void main(String[] args) { //2.实现Runnable接口 Thread t1=new Thread(new MyRunnable()); t1.start(); }
2.使用匿名内部类实现
Thread t2=new Thread(new Runnable() { @Override public void run() { System.out.println("使用Runnable接口,创建匿名内部类实现"); } }); t2.start();
<3>实现Callable接口
实现Callable重现call方法,允许抛出异常,允许带有返回值,返回数据类型为接口上的泛型
class MyCallable implements Callable<String> { //允许抛出异常,允许带有返回值,返回数据类型为接口上的泛型 @Override public String call() throws Exception { System.out.println("实现了Callable接口"); return "这不是一个线程类,而是一个任务类"; } } public static void main(String[] args) throws ExecutionException, InterruptedException { //方法三:实现Callable接口,是一个任务类 //FutureTask底层也实现了Runnable接口 FutureTask<String> task=new FutureTask<>(new MyCallable()); new Thread(task).start(); System.out.println(task.get()); }
二.线程的常用方法
1.构造方法和属性的获取方法
构造方法:
属性的获取方法:
2.常用方法
<1>run()和start()
start();方法:启动线程
run();方法:覆写 run 方法是提供给线程要做的事情的指令清单
start()和run()的区别:见代码
public class Thread_Run_VS_Start { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { while (true){ } } }).run(); /** * main线程直接调用Thread对象的run方法会直接在main线程 * 运行Thread对象的run()方法---->传入的runnable对象.run() * 结果,main线程直接运行while(true) * * start()是启动一个线程,调用新线程的while(true)方法 * 对比通过start()调用的结果区别 */ new Thread(new Runnable() { @Override public void run() { while (true){ } } }).start(); } }
<2>interrupt()方法
通过interrupt()方法,通知线程中的中断标志位,由false变为true,但是线程什么时候中断,需要线程自己的代码实现
通过线程中的中断标志位实现,比起自己手动设置中断标志位,可以避免线程处于阻塞状态下,无法中断的情况
对interrupt,isInterrupt,interrupted的理解:
实例方法:
(1)interrupt:置线程的中断状态
如果调用该方法的线程处于阻塞状态(休眠等),会抛出InterruptedException异常并且会重置Thread.interrupted;返回当前标志位,并重置(2)isInterrupt:线程是否中断,返回boolean 静态方法:(3)interrupted:返回线程的上次的中断状态,并清除中断状态
public class Interrupt { public static void main(String[] args) throws InterruptedException { Thread t=new Thread(new Runnable() { @Override public void run() { //...执行任务,执行时间可能比较长 //运行到这里,在t的构造方法中不能引用t使用Thread.currentThread()方法,获取当前代码行所在线程的引用 for (int i = 0; i <10000&&!Thread.currentThread().isInterrupted() ; i++) { System.out.println(i); //模拟中断线程 try { Thread.sleep(1000); //通过标志位自行实现,无法解决线程阻塞导致无法中断 //Thread,sleep(100000) } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start();//线程启动,中断标志位=false System.out.println("t start"); //模拟,t执行了5秒,进程没有结束,要中断,停止t线程 Thread.sleep(5000); //未设置时,isInterrupt为false //如果t线程处于阻塞状态(休眠等),会抛出InterruptedException异常 //并且会重置isInterrupt中断标志位位false t.interrupt();//告诉t线程,要中断(设置t线程的中断标志位为true),由t的代码自行决定是否要中断 //isInterrupt设置为true //t.isInterrupted(); Interrupted是线程中的标志位 System.out.println("t stop"); //注:Thread.interrupted(); 返回当前线程的中断标志位,然后重置中断标志位 } }
<3>join方法
注意: join方法是实例方法
等待一个线程执行完毕,才执行下一个线程(调用该方法的线程等待)
无参
:t.join:当前线程无条件等待,直到t线程运行完毕
有参
:t.join(1000)等待1秒,或者t线程结束,哪个条件满足,当前线程继续往下执行
//join方法:实例方法: // 1.无参:t.join:当前线程无条件等待,直到t线程运行完毕 // 2.有参:t.join(1000)等待1秒,或者t线程结束,哪个条件满足,当前线程继续往下执行 public class Join { public static void main(String[] args) throws InterruptedException { Thread t=new Thread(new Runnable() { @Override public void run() { System.out.println("1"); } }); t.start(); t.join();//当前线程main线程无条件等待,直到t线程执行完毕,当前线程再往后执行 // t.join(1000);当前线程等到1秒,或者等t线程执行完毕 System.out.println("ok"); } }
<4>获取当前线程的引用currentThread();方法
静态方法:
public class ThreadDemo { public static void main(String[] args) { Thread thread = Thread.currentThread(); System.out.println(thread.getName()); } }
<5>休眠当前线程sleep();方法
让线程等待一定时间后,继续运行
Thread.sleep(1000);
<6>线程让步yield();方法
让yield();所在代码行的线程让步,当其他线程先执行
public class Yield { public static void main(String[] args) { for(int i=0;i<20;i++){ final int n=i; Thread t=new Thread(new Runnable() { @Override public void run() { System.out.println(n); } }); t.start(); } //判断:如果活跃的线程数量大于1,main线程让步 while (Thread.activeCount()>1){//记录活跃线程的数量 Thread.yield(); }//注意:要用debug方式,因为run方式,idea后台还会启动一个线程 //实现ok在1到二十之后打印 System.out.println("ok"); } }
三.线程的生命周期和状态转换
Java 语言中线程共有六种状态,分别是:
NEW
(初始化状态)
RUNNABLE
(可运行 / 运行状态)
BLOCKED
(阻塞状态)
WAITING
(无时限等待)
TIMED_WAITING
(有时限等待)
TERMINATED
(终止状态)
生命周期和状态转换图:
常见的API导致的状态转换:
1.线程的阻塞:
Thread.sleep(long);当前线程休眠
t.join/t.join(long);t线程加入当前线程,当前线程等待阻塞
synchronized:竞争对象锁失败的线程,进入阻塞态
2.线程的启动:
start() ----->注意:run()只是任务的定义,start()才是启动
3. 线程的中断:interrupt让某个线程中断,不是直接停止线程,而是一个“建议”,是否中断,由线程代码自己决定
四.线程间的通信
wait(0方法
:线程等待 notify();方法
:随机唤醒一个线程 notifyAll():方法
:唤醒所有等待的线程 注意:这三个方法都需要被Synchronized包裹x
线程间通信的案例:
有三个线程,每个线程只能打印A,B或C
要求:同时执行三个线程,按ABC顺序打印,依次打印十次
ABC换行 ABC换行。。。。
public class SequencePrintHomeWork { //有三个线程,每个线程只能打印A,B或C //要求:同时执行三个线程,按ABC顺序打印,依次打印十次 //ABC换行 ABC换行。。。。 //考察知识点:代码设计,多线程通信 public static void main(String[] args) { Thread a = new Thread(new Task("A")); Thread b = new Thread(new Task("B")); Thread c = new Thread(new Task("C")); c.start(); b.start(); a.start(); } private static class Task implements Runnable{ private String content; //顺序打印的内容:可以循环打印 private static String[] ARR = {"A", "B", "C"}; private static int INDEX;//从数组哪个索引打印 public Task(String content) { this.content = content; } @Override public void run() { try { for(int i=0; i<10; i++){ synchronized (ARR){//三个线程使用同一把锁 //从数组索引位置打印,如果当前线程要打印的内容不一致,释放对象锁等待 while(!content.equals(ARR[INDEX])){ ARR.wait(); } //如果数组要打印的内容和当前线程要打印的一致, // 就打印,并把数组索引切换到一个位置,通知其他线程 System.out.print(content); if(INDEX==ARR.length-1){ System.out.println(); } INDEX = (INDEX+1)%ARR.length; ARR.notifyAll(); } } } catch (InterruptedException e) { e.printStackTrace(); } } } }
补充: wait()和sleep()的区别:
wait
之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对象上的 monitor lock
sleep
是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
wait
是 Object 的方法
sleep
是 Thread 的静态方法
五.多线程的安全及解决
1.原子性
对原子性的理解: 我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
注意: 一条 java 语句不一定是原子的,也不一定只是一条指令
例如:
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
2.可见性
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
可见性
:系统调度CPU执行线程内,某个方法,产生CPU视角的主存,工作内存
主存
:线程共享
工作内存
:线程私有内存+CPU高速缓存/寄存器
对主存中共享数据的操作,存在主存到工作内存<====>从主存读取,工作内存修改,写回主存(拷贝)
3.代码的顺序性
代码的重排序:
一段代码:
1.去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
代码重排序会给多线程带来什么问题:
刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。
4.线程不安全问题的解决
<1>synchronized 关键字
这里会在下面锁体系中详细说
<2>volatile 关键字
volatile 关键字的作用:
(1)保证可见性
(2)禁止指令重排序,建立内存屏障——单例模式说明
(3)不保证原子性
常见的使用场景:一般是读写分离的操作,提高性能
(1)写操作不依赖共享变量,赋值是一个常量(依赖共享变量的赋值不是原子性操作)
(2)作用在读,写依赖其他手段(加锁)
一个volatile的简单例子:
public class Test { private static boolean flag = true; public static void main(String[] args) { //创建一个线程并启动 new Thread(new Runnable() { int i=0; @Override public void run() { while(flag){ //这个语句底层使用了synchronized,保证了可见性 //System.out.println("============="); i++; } } }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //即使改了,上面的线程flag也不会改,会一直循环 flag = false; } }
六.锁体系
多线程中锁的作用:保证线程的同步
1.Synchronized加锁方式
<1>Synchronized的加锁方式及语法基础
如何解决上述原子性例子的问题:
是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
synchronized 关键字:
(1)作用:对一段代码进行加锁操作,让某一段代码满足三个特性:原子性,可见性,有序性
(2)原理:多个线程间同步互斥(一段代码在任意一个时间点,只有一个线程执行:加锁,释放锁)
注意: 加锁/释放锁是基于对象来进行加锁和释放锁,不是把代码锁了
只有对同一个对象加锁,才会让线程产生同步互斥的效果:
那么怎样才叫对同一个对象加锁呢?
这里t代表类名,t1,t2是 new了两个t increment是t中的一个方法(是静态还是实例具体看)
synchronized处加锁,抛出异常或代码块结束释放锁
具体过程:
synchronized 多个线程n同步互斥:
(1):一个时间只有一个线程执行(同步互斥)
(2):竞争失败的线程,不停的在阻塞态和运行态切换(用户态和内核态切换)
(3)同步线程数量越多,性能越低
一个简单的小例子:
public class SafeThread { //有一个遍历COUNT=0;同时启动20个线程,每个线程循环1000次,每次循环把COUNT++ //等待二十个子线程执行完毕之后,再main中打印COUNT的值 //(预期)count=20000 private static int COUNT=0; //对当前类对象进行加锁,线程间同步互斥 // public synchronized static void increment(){ // COUNT++; // } //使用不同的对象加锁,没有同步互斥的效果,并发并行 // public static void increment(){ // synchronized (new SafeThread()){ // COUNT++; // } // } public static void main(String[] args) throws InterruptedException { //尽量同时启动,不让new线程操作影响 Class clazz=SafeThread.class; Thread[]threads=new Thread[20]; for (int i = 0; i <20 ; i++) { threads[i]=new Thread(new Runnable() { @Override public void run() { for (int j = 0; j <1000 ; j++) { //给SafeThread对象加一把锁 synchronized (clazz){ COUNT++; } } } }); } for (int i = 0; i <20 ; i++) { threads[i].start(); } //让main线程等待20个子线程运行完毕 for (int i = 0; i <20 ; i++) { threads[i].join(); } System.out.println(COUNT); } }
synchronized加锁的缺点:
a)如果获取锁的线程由于要等待IO或其他原因(如调用sleep方法)被阻塞了,但又没有释放锁,其他线程只能干巴巴地等待,此时会影响程序执行效率。
b)只要获取了synchronized锁,不管是读操作还是写操作,都要上锁,都会独占。如果希望多个读操作可以同时运行,但是一个写操作运行,无法实现。
<2>Synchronized的原理及实现
1.Monitor机制:
(1)基于monitor对象的监视器:使用对象头的锁状态来加锁
(2)编译为字节码指令为:1个monitoren+2个monitorexit 多出来的一个monitorexit:如果出现异常,第一个monitorexit无法正确释放锁,这个monitorexit进行锁释放
例如下列代码:
public class Test1 { public Test1() { } public static void main(String[] args) { Class var1 = Test1.class; synchronized(Test1.class) { System.out.println("hello"); } } }
反编译:
(3)monitor存在计数器实现synchronized的可重入性:进入+1,退出-1;
<3>JVM对Synchronized的优化
(1).对锁的优化
Synchronized是基于对象头的锁状态来实现的,从低到高:(锁只能升级不能降级)
(1)无锁
(2)偏向锁:对同一个对象多次加锁(重入)
(3)轻量级锁:基于CAS实现,同一个时间点,经常只有一个线程竞争锁
(4)重量级锁:基于系统的mutex锁,同一个时间点,经常有多个线程竞争
特点:mutex是系统级别的加锁,线程会由用户态切换到内核态,切换的成本比较高(一个线程总是竞争失败,就会不停的在用户态和内核态之间切换,比较耗费资源,进一步,如果很多个竞争失败的线程,性能就会有很大的影响)
(2).锁粗话
多个synchronized连续执行加锁,释放锁,可以合并为一个
示例:StringBuffer静态变量,在一个线程中多次append(静态变量属于方法区,jdk 1.8后是在堆里面,线程共享)
public class Test { private static StringBuffer sb; public static void main(String[] args) { sb.append("1").append("2").append("3"); } }
(3).锁消除
对不会逃逸到其他线程的变量,执行加锁的操作,可以删除加锁
示例:StringBuffer局部变量,在一个线程中多次append(局部变量属于虚拟机栈,是线程私有的)
public class Test { public static void main(String[] args) { StringBuffer sb=new StringBuffer(); sb.append("1"); sb.append("2"); sb.append("3"); } }
2.常见的锁策略及CAS
多线程中锁类型的划分:
API层面:
synchronized加锁 Lock加锁
锁的类型
:偏向锁,轻量级锁,重量级锁,自旋锁,独占锁,共享锁,公平锁,非公平锁等等
<1>.乐观锁和悲观锁
乐观锁和悲观锁的设计思想(和语言是无关的,不是java多线程独有的)
根据使用常见来阐述:
乐观锁
:同一个时间点,经常只有一个线程来操作共享变量,适合使用乐观锁
悲观锁
:同一个时间点,经常有多个线程来操作共享变量,适合使用悲观锁
乐观锁的实现原理:
通过直接操作共享变变量(不会阻塞),通过调用的api的返回值,来知道操作是成功还是失败的 java多线程的实现:基于CAS的方式实现(Compare and Swap)
令:主存中需要操作的变量为V,线程A的工作内存中,读入A,修改为N
有另一个线程可能对主存中的V进行操作
此时:新的主存中操作的变量令为O,比较线程A中的V和此时主存中的O是否相等,如果相等,说明可以将N写回主存,如果不相等,任务主存中的变量被B线程操作过,此时A中的N不写入主存,线程A不做任何事情。
悲观锁的实现原理:类似于synchronized加锁方式
**CAS中可能存在的问题(ABA问题) **
肯主存中原来的V值,被线程B加一,再减一,依然满足上述线程A可以写入N的条件
解决办法:为主存中的变量加上一个版本好,在上诉A线程可写入的基础上,再比较一次版本好。即可解决。
CAS在java中是使用unsafe类来完成的,本质上是基于CPU提供的对变量原子性线程安全的修改操作
<2>自旋锁
按照普通加锁的方式处理,当线程在抢锁失败之后会进入阻塞状态,放弃CPU,需要经过很久才能被再次调度,所以,引入读写锁,当锁竞争失败之后,只需要很短时间,锁就能再次被释放,此时,让竞争失败的线程,进入自旋,不在用户态和内核态之间切换。只要没抢到锁,就死等。
类似以下代码:
<1>.无条件的自选:
while(抢锁(lock)==失败{}
自旋锁的缺陷:如果之前的假设(锁很快就能被释放)没有满足,那么进入自旋的线程就一直在消耗CPU的资源,长期在做无用功
<2>.有条件的自旋:
如可中断的自旋:自旋时线程判断中断标志位后再执行,或者限制自旋的次数,限制自旋的时间
自旋锁,悲观乐观锁,CAS的总结:
<1>.悲观锁是线程先加锁,之后再修改变量的操作
<2>.乐观锁是线程直接尝试修改变量(不会阻塞)。在java多线程中是基于CAS 实现的。
<3>.CAS
概念:Compare and Swap比较并交换
实现/原理:基于unsafe来实现,本质上是基于CPU提供的接口保证线程安全修改变量。
使用(V,O,N):V为内存地址中存放的实际值,O为预期的值(旧值),N为更新的值(新值)
可能出现的问题:ABA问题(引入版本号解决)
<4>.自旋+CAS
适用的场景:同一个时间点,常常只有一个线程进行操作
不适应的场景:
1.同一个时间点,常常有多个线程进行操作
2.CAS的操作时间时间太长,给了其他线程操作共享变量的机会,那么CAS的成功率会很低,经常做无用功
自旋的缺陷:线程一直处于运行态,会很耗费CPU的资源
<3>可重入锁
允许同一个线程多次获取同一把锁
java中只要以Reentrant开头命名的锁都是可重入的锁,现有的jdk提供的lock的实现类和synchronized加锁,都是可重入锁例如:
public class Test2 { public static synchronized void t1(){ t2(); } public static synchronized void t2(){ } public static void main(String[] args) { t1(); } }
3.Lock体系
<1>Lock接口
(1)使用Lock锁实现线程同步
上代码!
public class AccountRunnable implements Runnable { private Account account = new Account(); //买一把锁 Lock lock = new ReentrantLock(); //Re-entrant-Lock 可重入锁 @Override public void run() { //此处省略300句 try{ //上锁 lock.lock(); //判断余额是否足够,够,取之;不够,不取之; if(account.getBalance()>=400){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } method1(); //取之 account.withDraw(400); //输出信息 System.out.println(Thread.currentThread().getName()+ "取款成功,现在的余额是"+account.getBalance()); }else{ System.out.println("余额不足,"+Thread.currentThread().getName() +"取款失败,现在的余额是" +account.getBalance()); } }finally { //解锁 lock.unlock(); } //此处省略100句 } }
这里要注意:释放锁时,要考虑是否出现异常,和上面synchronized加锁相同,要进行两次锁释放,这里将锁放在finally代码块中
(2)Lock加锁的四种方式
形象记忆:男生追女生
1.lock()
:一直表白,直到成功
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
2.tryLock()
:表白一次,失败就放弃
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。拿不到锁时不会一直在那等待。
3.tryLock
(long time, TimeUnit unit) 在一定的时间内持续表白,如果时间到了则放弃
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
4.lockInterruptibly()
一直表白,当被通知她有男朋友了,才放弃 lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
也就使说,当这个线程使用lockInterruptibly()获取锁,当被interrupt中断时,才会停止竞争锁
<2>AQS简单认识
AQS: AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。这个类在java.util.concurrent.locks包.
AQS的核心思想是: 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS的实现方式:
如图示,AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。state就是共享资源
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
<3>ReentrantLock
(1)ReentrantLock基本概念
ReentrantLock
,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的非内部类,并且ReentrantLock提供了更多的方法。
ReentrantLock锁
在同一个时间点只能被一个线程锁持有。
ReentraantLock
是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。
当单个线程或线程交替执行时,他与队列无关,只会在jdk级别解决,性能高
(2)自己实现一个简单的ReentrantLock
原理:自旋+park–unpark+CAS
public class Test2 { volatile int status=0; Queue parkQueue;//集合 数组 list void lock(){ while(!compareAndSet(0,1)){ //这里不能用sleep或yield实现 //sleep无法确定睡眠的时间 //yield只能用于两个线程竞争,当有多个线程之后,t1抢不到锁,yield会让出cpu,但是可能下一次cpu还是调t1 park(); } unlock(); } void unlock(){ lock_notify(); } void park(){ //将当期线程加入到等待队列 parkQueue.add(currentThread); //将当期线程释放cpu 阻塞 睡眠 releaseCpu(); } void lock_notify(){ //status=0 //得到要唤醒的线程头部线程 Thread t=parkQueue.header(); //唤醒等待线程 unpark(t); } }
(3)ReentrantLock部分源码分析
ReentrantLock锁分为公平锁和非公平锁(创建不加参数时默认非公平锁)
ReentrantLock提供了两个构造器:
//非公平锁 public ReentrantLock() { sync = new NonfairSync(); } //公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
ReentrantLock的lock方式:
非公平锁:
调用lock方法:
final void lock() { if (compareAndSetState(0, 1))//首先用一个CAS操作,判断state是否是0(表示当前锁未被占用) setExclusiveOwnerThread(Thread.currentThread());//设置当前占有锁的线程为该线程 else acquire(1); }
首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
下面说说acquire的过程
public final void acquire(int arg) { //首先看看自己要不要排队,如果不用排队,获取锁,要排队,加入AQS队列 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
(1)尝试去获取锁(看看自己要不要排队)
非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。
tryAcquire(arg) final boolean nonfairTryAcquire(int acquires) { //获取当前线程 final Thread current = Thread.currentThread(); //获取state变量值 int c = getState(); if (c == 0) { //没有线程占用锁 if (compareAndSetState(0, acquires)) { //占用锁成功,设置独占线程为当前线程 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁 重入锁 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // 更新state值为新的重入次数 setState(nextc); return true; } //获取锁失败 return false; }
(2)入队根据java运算符短路,如果不需要排队,方法直接返回,如果需要排队,进入addWaiter方法
公平锁:
公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1)
<4>ReadWriteLock锁
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
ReadWriteLock
是一个接口,ReentrantReadWriteLock是它的实现类,该类中包括两个内部类ReadLock和WriteLock,这两个内部类实现了Lock接口。
认识ReadWriteLock锁
public class TestLock { public static void main(String[] args) { //默认也是非公平锁 也是可重入锁 ReadWriteLock rwl = new ReentrantReadWriteLock(); //多次返回的都是同一把读锁 同一把写锁 Lock readLock = rwl.readLock(); Lock readLock2 = rwl.readLock(); Lock writeLock = rwl.writeLock(); readLock.lock(); readLock.unlock(); System.out.println(readLock==readLock2); } }
注意:从结果中看到,从一个ReadWriteLock中多次获取的ReadLock、WriteLock是同一把读锁,同一把写锁。
4.Lock锁和同步锁(synchronized)的区别
5.死锁
先上代码:
package threadadvanced.lesson1; class Pen { private String pen = "笔" ; public String getPen() { return pen; } } class Book { private String book = "本" ; public String getBook() { return book; } } public class DeadLock { private static Pen pen = new Pen() ; private static Book book = new Book() ; public static void main(String[] args) { new DeadLock().deadLock(); } public void deadLock() { Thread thread1 = new Thread(new Runnable() { // 笔线程 @Override public void run() { synchronized (pen) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread()+" :我有笔,我就不给你"); synchronized (book) { System.out.println(Thread.currentThread()+" :把你的本给我!"); } } } },"Pen") ; Thread thread2 = new Thread(new Runnable() { // 本子线程 @Override public void run() { synchronized (book) { System.out.println(Thread.currentThread()+" :我有本子,我就不给你!"); synchronized (pen) { System.out.println(Thread.currentThread()+" :把你的笔给我!"); } } } },"Book") ; thread1.start(); thread2.start(); } }
出现死锁:
jconsole检查死锁:
1.死锁出现的原因:
至少两个线程,互相持有对方需要的资源没有释放,再次申请对方以及持有的资源
2.出现死锁的后果:
线程互相阻塞等待地方的资源,会一直处于阻塞等待的状态
3.如何检测死锁:
使用jdk工具:jconsole(查看线程)---->jstack
4.解决死锁的方法:
(1)资源一次性分配(破坏请求与保持条件)
(2)在满足一定条件的时候,主动释放资源
(3)资源的有序分配:系统为每一类资源赋予一个编号,每个线程按照编号递请求资源,释放则相反
七.多线程案例
1.生产者消费者问题
示例:
面包店
10个生产者,每个每次生产3个
20个消费者,每个每次消费一个
进阶版需求
面包师傅每个最多生产30次,面包店每天生产10303=900个面包
消费者也不是一直消费。把900个面包消费完结束
隐藏信息:面包店每天生产面包的最大数量为900个
消费者把900个面包消费完结束
代码示例:
/** * 面包店 * 10个生产者,每个每次生产3个 * 20个消费者,每个每次消费一个 * * 进阶版需求 * 面包师傅每个最多生产30次,面包店每天生产10*30*3=900个面包 * 消费者也不是一直消费。把900个面包消费完结束 * * 隐藏信息:面包店每天生产面包的最大数量为900个 * 消费者把900个面包消费完结束 */ public class AdvancedBreadShop { //面包店库存数 private static int COUNT; //面包店生产面包的总数,不会消费的 private static int PRODUCE_NUMBER; public static class Consumer implements Runnable{ private String name; public Consumer(String name) { this.name = name; } @Override public void run() { try { while (true){ synchronized (AdvancedBreadShop.class){ if(PRODUCE_NUMBER==900&&COUNT==0){ System.out.println("今天面包已经卖完了"); break; }else { if(COUNT==0){ AdvancedBreadShop.class.wait(); }else { System.out.printf("%s消费了一个面包\n",this.name); COUNT--; AdvancedBreadShop.class.notifyAll(); Thread.sleep(100); } } } Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } } } private static class Producer implements Runnable{ private String name; public Producer(String name) { this.name = name; } @Override public void run() { try { //生产者生产30次,结束循环 for(int i=0;i<=30;i++) { synchronized (AdvancedBreadShop.class){ if(i==30){ System.out.println("今天面包生产完了"); break; }else { if(COUNT>97){ AdvancedBreadShop.class.wait(); }else { COUNT=COUNT+3; PRODUCE_NUMBER=PRODUCE_NUMBER+3; System.out.printf("%s生产了三个面包\n",this.name); AdvancedBreadShop.class.notifyAll(); Thread.sleep(100); } } } Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread[] Consumers=new Thread[20]; Thread[] Producers=new Thread[10]; for (int i = 0; i <20 ; i++) { Consumers[i]=new Thread(new Consumer(String.valueOf(i))); } for (int i = 0; i <10 ; i++) { Producers[i]=new Thread(new Producer(String.valueOf(i))); } for (int i = 0; i <20 ; i++) { Consumers[i].start(); } for (int i = 0; i <10 ; i++) { Producers[i].start(); } } }
2.单例模式
基于单例模式下的懒汉模式(双重校验锁实现)(多线程版,二次判断,效率高)代码示例:
public class Singleton { //volatile关键字修饰,保证的可见性和代码的顺序性 private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { //判断instance是否为空,竞争锁的条件 if (instance == null) { //保证线程安全,为Singleton.class加锁 synchronized (Singleton.class) { //再次判断instance是否为空,防止多个线程进入第一个if后 //对synchronized锁竞争失败进入阻塞状态后,再次进入运行态时 //new了多个Singleton,不符合单例模式 //保证线程安全 if (instance == null) { instance = new Singleton(); } } } return instance; } }
3.阻塞式队列
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
阻塞式队列代码实现:
/** * 实现阻塞队列 * 1.线程安全问题:在多线程情况下,put,take不具有原子性,4个属性,不具有可见性 * 2.put操作:如果存满了,需要阻塞等待。take操作:如果是空,阻塞等待 * @param <T> */ public class MyBlockingQueue <T>{ //使用数组实现循环队列 private Object[] queue; //存放元素的索引 private int putIndex ; //取元素的索引 private int takeIndex; //当前存放元素的数量 private int size; public MyBlockingQueue(int len){ queue=new Object[len]; } //存放元素,需要考虑: //1.putIndex超过数组长度 //2.size达到数组最大长度 public synchronized void put(T e) throws InterruptedException { //不满足执行条件时,一直阻塞等待 //当阻塞等待都被唤醒并再次竞争成功对象锁,回复往下执行时,条件可能被其他线程修改 while (size==queue.length){ this.wait(); } //存放到数组中放元素的索引位置 queue[putIndex]=e; putIndex=(putIndex+1)%queue.length; size++; notifyAll(); } //取元素 public synchronized T take() throws InterruptedException { while (size==0){ this.wait(); } T t= (T) queue[takeIndex]; queue[takeIndex]=null; takeIndex=(takeIndex+1)%queue.length; size--; notifyAll(); return t; } public int size(){ return size; } public static void main(String[] args) { MyBlockingQueue<Integer>queue=new MyBlockingQueue<>(10); //多线程的调试方式:1.写打印语句 2.jconsole for (int i = 0; i <3 ; i++) { new Thread(new Runnable() { @Override public void run() { try { for (int j = 0; j <100 ; j++) { queue.put(j); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } for (int i = 0; i <3 ; i++) { new Thread(new Runnable() { @Override public void run() { try { while (true){ int t= queue.take(); System.out.println(Thread.currentThread().getName()+":"+t); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } }
4.线程池
线程池最大的好处就是减少每次启动、销毁线程的损耗
import java.util.concurrent.*; public class ThreadPoolExecutorTest { public static void main(String[] args) { //以快递公司,快递员,快递业务为模型 ThreadPoolExecutor pool=new ThreadPoolExecutor( 5,//核心线程数---->正式员工数 10,//最大线程数-->正式员工+临时员工 60,//临时工的最大等待时间 TimeUnit.SECONDS,//idle线程的空闲时间-->临时工最大的存活时间,超过就解雇 new LinkedBlockingQueue<>(),//阻塞队列,任务存放的地方--->快递仓库 new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(new Runnable() { @Override public void run() { //r对象是线程池内部封装过的工作任务类(Worker),会一直循环等待的方式从阻塞队列中拿取任务并执行 //所以不能调用r.run();方法 System.out.println(Thread.currentThread().getName()+"开始执行了"); } }); } },//创建线程的工厂类 线程池创建线程时,调用该工厂类的方法创建线程(满足该工厂创建线程的要求) //---->对应招聘员工的标准 /** * 拒绝策略:达到最大线程数且阻塞队列已满,采取拒绝策略 * AbortPolicy:直接抛出RejectedExecutionException(不提供handler时的默认策略) * CallerRunsPolicy:谁(某个线程)交给我(线程池)的任务,我拒绝执行,由谁自己去执行 * DiscardPolicy:交给我的任务直接丢弃掉 * DiscardOldestPolicy:阻塞队列中最旧的任务丢弃 */ new ThreadPoolExecutor.AbortPolicy()//拒绝策略-->达到最大线程数,且阻塞队列已满,采取的拒绝策略 );//线程池创建以后,只要有任务们就会自动执行 for (int i = 0; i <20 ; i++) { //线程池执行任务:execute方法,submit方法--->提交执行一个任务 //区别:返回值不同 pool.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } //线程池有4个快捷的创建方式(实际工作不使用,作为面试了解) //实际工作需要使用ThreadPoolExecutor,构造参数是我们自己指定,比较灵活 ExecutorService pool2=Executors.newSingleThreadExecutor();//创建单线程池 ExecutorService pool3=Executors.newCachedThreadPool();//缓存的线程池 ExecutorService pool5=Executors.newFixedThreadPool(4);//固定大小线程池 ScheduledExecutorService pool4=Executors.newScheduledThreadPool(4);//计划任务线程池 //两秒中之后执行这个任务 pool4.schedule(new Runnable() { @Override public void run() { System.out.println("hello"); } }, 2, TimeUnit.SECONDS); //一直执行任务 pool4.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("hello"); } }, 2, 1,TimeUnit.SECONDS);//比如一个脑子,两秒后开始叫我,然后每隔一秒叫我一次 } }
八.总结
(1)代码块锁是一个防止数据发生错误的一个重要手段;
(2)对象的统一性是非常重要的,这要想到对象的传入问题,要操作的对象只能new一次,其他的操作都是对这个传入的对象进行的,才能保证数据一致性,完整性和正确性。
到此这篇关于一篇文章让java多线程从入门到精通的文章就介绍到这了,更多相关java多线程从入门到精通内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!