文章目录
一、并发编程存在的线程不安全问题
1.什么是线程不安全?
- 多线程并发执行某个代码时,产生了逻辑上的错误,结果和预期值不相同的情况
2.造成线程不安全问题的原因是什么?
CPU抢占临界区的共享资源(最根本原因),具体表现如下:
1.非原子性
- 当 cpu 执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束
在这里介绍一下
原子性
:
举个例子:
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。
我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。
如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。
2.内存不可见性
- 当多个线程操作共享数据时,彼此不可见
因为T线程要频繁的从主内存中读取flag的值,JIT编译器会将flag的值缓存在高速的工作内存中,减少对主内存中flag的访问,提高效率。
1秒后main线程修改了flag的值,并同步到主内存中,而T线程是从工作内存的高速缓存中读取到这个变量的值,结果永远是旧值。
3.指令重排序
- java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,来提高程序的运行效率。如果是单核CPU,则不存在这个问题,同时还会提高执行效率
二、线程不安全问题的解决方案
1.synchronized 关键字
1. synchronized原理
synchronized的底层是使用操作系统的mutex lock实现的。
- 用对象锁保险证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断,采用互斥的方式让同一时刻最多有一个线程能持有锁,其他线程想获取这个对象锁时就会阻塞住。这样可以保证拥有锁的线程可以安全的
2. synchronized使用场景
在没有加锁的情况下
public class test1 {
//count用于计数
static int count = 0;
public static void main(String[] args) throws InterruptedException {
//创建线程1
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
}, "t1");
//创建线程2
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
}, "t2");
//启动线程
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
运行结果
我们发现在没有加锁的情况下,执行的结果不等于0(正确结果为0)
下面演示通过加锁输出正确的结果
public class test2 {
//count用于计数
static int count = 0;
//创建锁对象
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程1
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
count++;
}
}
}, "t1");
//创建线程2
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
count--;
}
}
}, "t2");
//启动线程
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
运行结果
3. 线程八锁
考察synchronized锁住的是哪个对象,是否存在互斥
- 情况1:两个普通同步方法,两个线程,标准打印, 打印?
public class Test3 {
private static class Numner {
public synchronized void a() {
System.out.println("one");
}
public synchronized void b() {
System.out.println("two");
}
}
public static void main(String[] args) {
//创建锁对象
Numner numner = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.b();
}
}).start();
}
}
运行结果
- 情况2:新增 Thread.sleep() 给 a() ,打印?
public class Test4 {
private static class Numner {
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void b() {
System.out.println("two");
}
}
public static void main(String[] args) {
Numner numner = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.b();
}
}).start();
}
}
运行结果
- 情况3:新增普通方法 c() , 打印?
public class Test5 {
private static class Numner {
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void b() {
System.out.println("two");
}
public void c() {
System.out.println("three");
}
}
public static void main(String[] args) {
Numner numner = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.b();
}
}).start();
//创建第三个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.c();
}
}).start();
}
}
运行结果
- 情况4:两个普通同步方法,两个 Number 对象,打印?
public class Test6 {
private static class Numner {
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void b() {
System.out.println("two");
}
}
public static void main(String[] args) {
Numner numner = new Numner();
Numner numner1 = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner1.b();
}
}).start();
}
}
运行结果
- 情况5:修改 a() 为静态同步方法,打印?
public class Test7 {
private static class Numner {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void b() {
System.out.println("two");
}
}
public static void main(String[] args) {
Numner numner = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.b();
}
}).start();
}
}
运行结果
- 情况6:修改两个方法均为静态同步方法,一个 Number 对象?
public class Test8 {
private static class Numner {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public static synchronized void b() {
System.out.println("two");
}
}
public static void main(String[] args) {
Numner numner = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.b();
}
}).start();
}
}
运行结果
- 情况7:一个静态同步方法,一个非静态同步方法,两个 Number 对象?
public class Test9 {
private static class Numner {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void b() {
System.out.println("two");
}
}
public static void main(String[] args) {
Numner numner = new Numner();
Numner numner1 = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner1.b();
}
}).start();
}
}
运行结果
- 情况8:两个静态同步方法,两个 Number 对象?
public class Test10 {
private static class Numner {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public static synchronized void b() {
System.out.println("two");
}
}
public static void main(String[] args) {
Numner numner = new Numner();
Numner numner1 = new Numner();
//创建第一个线程
new Thread(new Runnable() {
@Override
public void run() {
numner.a();
}
}).start();
//创建第二个线程
new Thread(new Runnable() {
@Override
public void run() {
numner1.b();
}
}).start();
}
}
运行结果
4. Java的对象头和对象组成
Java对象保存在内存中时,由以下三部分组成:
1、对象头
2、实例数据
3、对齐填充字节
对象头
java的对象头由以下三部分组成:
1,Mark Word
2,指向类的指针
3,数组长度(只有数组对象才有)
- Mark Word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
-
当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
-
当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
-
当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
-
当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
-
偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
-
轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
-
自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
5. synchronized锁优化、锁升级过程
- synchronized锁有四种状态,无锁,偏向锁,轻量级锁,重量级锁
- 锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态
偏向锁:
偏向==偏心、专用 原因:
轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作
使用场景:只有第一次使用CAS将线程ID设置对象的Mark Word头,之后再次访问时发现向合格线程是自己的,并且没有竞争,那么就不用重新CAS。以后只要不发生竞争,这个对象就归这个线程所有。轻量级锁:
如果一个对象虽然有多个线程访问,但是线程访问的时间是错开的(也就是没有竞争)那么可以使用轻量级锁锁膨胀:
如果尝试轻量级加锁的过程中,CAS无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。自旋优化:
并不属于锁。重量级锁竞争的时候,还可以使用自旋来进行优化,如果目前线程自旋成功(即这时候锁线程已经推出了同步块,释放了锁),当前线程就可以避免阻塞。(适用于多核CPU)重量级锁:
java最原始的同步锁。抢不到锁的线程就会被阻塞,在线程池中等待激活锁消除:
锁消除是发生在编译器级别的一种锁优化方式。有时候我们写的代码完全不需要加锁,却执行了加锁操作。指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁粗化:
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
6.synchronized的具体应用
1. wait¬ify
- wait()----一直等待
public class Test11 {
//创建锁对象
static final Object lock = new Object();
public static void main(String[] args) {
//创建线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
System.out.println("我睡着了...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
}
}
运行结果
- wait(long n)----等待一定时间后自动唤醒
public class Test12 {
//创建锁对象
static final Object lock = new Object();
public static void main(String[] args) {
//创建线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行...");
synchronized (lock) {
try {
lock.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行其他代码...");
}
}
}).start();
}
}
运行结果
- wait()¬ify()----唤醒某一个线程,无法指定
public class Test13 {
//创建锁对象
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程1
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行...");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行其他代码1....");
}
}
}).start();
//创建线程2
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行...");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行其他代码2....");
}
}
}).start();
Thread.sleep(2000);
System.out.println("唤醒上面的线程...");
synchronized (lock){
lock.notify();
}
}
}
运行结果
- wait()¬ifyAll()----唤醒所有的线程
public class Test14 {
//创建锁对象
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程1
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行...");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行其他代码1....");
}
}
}).start();
//创建线程2
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行...");
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行其他代码2....");
}
}
}).start();
Thread.sleep(2000);
System.out.println("唤醒上面的线程...");
synchronized (lock){
// lock.notify();
lock.notifyAll();
}
}
}
运行结果
- wait(long n)¬ify/notifyAll()----等待一定时间后被迫唤醒
public class Test15 {
//创建锁对象
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行...");
synchronized (lock){
try {
lock.wait(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行其他代码...");
}
}
}).start();
Thread.sleep(2000);
synchronized (lock){
lock.notify();
System.out.println("唤醒其他线程");
}
}
}
运行结果
执行...
//两秒后执行
唤醒其他线程
执行其他代码...
Process finished with exit code 0
本来4秒后自定唤醒,但是使用notify之后,2秒之后就被迫唤醒了
- wait¬ify使用注意点:
wait¬ify方法的调用必须处在该对象的锁(Monitor)中,也即,在调用这些方法时首先需要获得该对象的锁。否则会出现IllegalMonitorStateException异常。
- sleep(long n)和wait(long n)的区别
(1)sleep是Thread方法,而wait是Object方法
(2)sleep不强制要求和synchronize配合使用,但wait需要和synchronize一起使用
(3)sleep在睡眠的时候不会释放对象的,但是wait在等待时会释放对象锁。因为sleep() 是static静态的方法,他不能改变对象的机锁
(4)它们的状态都是TIMED_WAITING
- sleep在睡眠的时候不会释放对象的
public class Test16 {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("t1执行...");
synchronized (lock) {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始其他线程");
}
}, "t1").start();
Thread.sleep(1000);
synchronized (lock) {
System.out.println("main执行...");
}
}
}
运行结果
经过50秒后
才会主动唤醒,但在睡眠的过程中会一直持有锁,不会释放
再例如:
public class Test17 {
static final Object lock = new Object();
//有没有烟
static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
//创建线程1
new Thread(() -> {
synchronized (lock) {
System.out.println("有没有烟?:" + hasCigarette);
if (!hasCigarette) {
System.out.println("没烟,休息下");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("有没有烟?:" + hasCigarette);
if (hasCigarette) {
System.out.println("小南可以干活了");
}
}
}
}, "小南").start();
//创建线程2
new Thread(() -> {
System.out.println("小女可以干活吗:" + hasCigarette);
synchronized (lock) {
System.out.println("小女可以干活了");
}
}, "小女").start();
//创建线程3
Thread.sleep(1000);
new Thread(() -> {
//同一时刻只能有一个对象获得锁,此时小南正在睡觉,并且持有锁,送烟的没有办法获得锁
// synchronized (lock){
hasCigarette = true;
System.out.println("烟来了");
// }
}, "送烟的").start();
}
}
运行结果
存在的缺点:
- 1.其他干活的线程都要一直阻塞,效率低
- 2.小南必须两秒才能醒来,即使烟到了也无法唤醒
- 3.加了synchronize(lock),就好像小南睡觉反锁了门,烟根本没法进来
- 解决方法:使用wait-notify机制
public class Test18 {
static final Object lock = new Object();
//有没有烟
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (lock) {
System.out.println("有没有烟?" + hasCigarette);
while (!hasCigarette) {
System.out.println("没烟,休息下");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("有没有烟?" + hasCigarette);
if (hasCigarette) {
System.out.println("小南可以干活了");
} else {
System.out.println("活没干成1");
}
}
}
}, "小南").start();
new Thread(() -> {
System.out.println("外卖送到了没" + hasTakeout);
synchronized (lock) {
if (!hasTakeout) {
System.out.println("没外卖,先歇歇");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("外卖送到了没" + hasTakeout);
if (hasTakeout) {
System.out.println("小女可以干活了");
} else {
System.out.println("活没干成2");
}
}
}
}, "小女").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (lock) {
hasTakeout = true;
hasCigarette = true;
System.out.println("外卖到了、烟到了");
lock.notifyAll();
}
}, "送外卖和烟的").start();
}
}
运行结果
只要外卖到了、烟到了,小女和小南就会立刻起来干活,效率得到提高。
2.LockSupport类
1. park&unpark
public class Test19 {
public static void main(String[] args) throws InterruptedException {
//创建线程t1
Thread t1 = new Thread(() -> {
System.out.println("park");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
System.out.println("继续执行...");
LockSupport.park();
System.out.println("又可以执行了");
}, "t1");
t1.start();
Thread.sleep(1000);
System.out.println("unpark...");
LockSupport.unpark(t1);
}
}
运行结果
park
//遇到park就会暂停下来
unpark...
//知道unpark之后
继续执行...
从运行结果上我们发现System.out.println("又可以执行了");
并没有输出,原因是unpark只能使用一次。下面我们从它的底层原理上分析
1.每个线程都有一个自己的Parker对象,对象里有三个东西mutex,cond,counter
2.调用park的时候,线程看一下counter是不是0,是0,就阻塞在cond里,并再赋值一下0给counter,调用unpark,将counter赋值为1,并唤醒cond里的线程,然后再把counter置为0,线程恢复运行。
3.先调用unpark的情况,先置为1,调用park时,发现counter是1,不需要阻塞,继续运行,并把counter置为0.
- park&unpark与wait¬ify的区别
(1)wait¬ify必须在锁 对象中使用,而park&unpark不用
(2)park&unpark是以线程为单位进行【阻塞】和【唤醒】线程,可以精确唤醒具体的线程。而notify只能随机唤醒一个线程,notifyAll是唤醒所有的线程,不精确
(3)park&unpark可以先unpark,而wait¬ify不能先notify
(4)park不会释放锁资源,wait会释放锁资源
3.ReentrantLock类
ReentrantLock:可重入锁
是指同一个线程首次获得了这把锁,那么再次执行此线程就不需要再次获取锁。
1.可重入
public class Test20 {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("enter main");
m1();
} finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
System.out.println("enter m1");
m2();
} finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try {
System.out.println("enter m2");
} finally {
lock.unlock();
}
}
}
运行结果
2.可打断----使用lock&Interruptibly方法
public class Test21 {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1尝试获取锁....");
try {
//如果没有竞争,lock就会获取到锁
//如果有竞争,就会进入阻塞队列,可以用interrupt打断
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("没有锁,返回");
return;
}
try {
System.out.println("获取到锁");
} finally {
lock.unlock();
}
}
});
t1.start();
//给主线程加锁,此时两个对象都在获取锁,因此造成阻塞队列
System.out.println("主线程获取到锁");
lock.lock();
Thread.sleep(1000);
System.out.println("打断t1");
t1.interrupt();
}
}
运行结果
3.锁超时----tryLock()----支持被打断,抛出异常
public class Test22 {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1尝试获取锁...");
try {
if (!lock.tryLock(1,TimeUnit.SECONDS)) {
System.out.println("t1获取锁失败,返回");
return;
}
} catch (InterruptedException e) {
System.out.println("t1获取锁失败,返回");
return;
}
try {
System.out.println("t1获取锁成功");
} finally {
lock.unlock();
}
}
},"t1");
t1.start();
//主线程先一步获取到了锁,t1只能等待,tryLock等不到就只能返回false
System.out.println("主线程获取到锁");
lock.lock();
Thread.sleep(500);
lock.unlock();
System.out.println("主线程释放了锁");
}
}
运行结果
4.await()&signal()
public class Test23 {
//创建实例
private static final ReentrantLock ROOM = new ReentrantLock();
//创建等烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
//创建等外卖的休息室
static Condition waitTakeOut = ROOM.newCondition();
//有没有烟
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ROOM.lock();
try {
System.out.println("有没有烟?" + hasCigarette);
while (!hasCigarette) {
System.out.println("没烟,休息下");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("有没有烟?" + hasCigarette);
if (hasCigarette) {
System.out.println("小南可以干活了");
} else {
System.out.println("活没干成1");
}
}
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
System.out.println("外卖送到了没" + hasTakeout);
ROOM.lock();
try {
if (!hasTakeout) {
System.out.println("没外卖,先歇歇");
try {
waitTakeOut.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("外卖送到了没" + hasTakeout);
if (hasTakeout) {
System.out.println("小女可以干活了");
} else {
System.out.println("活没干成2");
}
}
} finally {
ROOM.unlock();
}
}, "小女").start();
Thread.sleep(1000);
new Thread(() -> {
ROOM.lock();
try {
System.out.println("外卖到了");
hasTakeout = true;
waitTakeOut.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
Thread.sleep(1000);
new Thread(() -> {
ROOM.lock();
try {
System.out.println("烟到了");
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
运行结果
- await&signal与wait¬ify的比较
相同点
- await&signal和wait¬ify都必须在锁对象里面使用
- await&signal和wait¬ify都可以释放锁资源
不同点
- await&signal唤醒操作可以精确到具体的线程,而wait¬ify不可以
- await&signal每次使用后都要手动释放锁,而wait¬ify不用
- synchronize和Lock之间的区别
1、synchronize可以修饰代码块,静态方法,普通方法,而lock只能修饰代码块
2、synchronize只有非公平锁,而lock有非公平锁,公平锁(ReentrantLock默认是非公平锁,也可以通过构造函数设置true声明为公平锁)
3、ReentrantLock更加的灵活(如tryLock)
4、synchronize是自动加锁和释放锁,而ReentrantLock需要手动加锁和释放锁
到目前为止关于唤醒的方法我们已经接触到了3种,分别是await&signal
,wait¬ify
,park&unpark
。
现在我们做个练习感受一下:
顺序打印2 1 ----wait()¬ify()
public class Test24 {
private static final Object lock = new Object();
static boolean blog = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
while (!blog) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("1");
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
blog = true;
System.out.println("2");
lock.notify();
}
}
}, "t2");
t2.start();
}
}
运行结果
顺序打印2 1 ----park()&unpark()
import java.util.concurrent.locks.LockSupport;
public class Test25 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
LockSupport.park();
System.out.println("1");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("2");
LockSupport.unpark(t1);
}
});
t2.start();
}
}
运行结果
顺序打印2 1 ----ReentrantLock----await()&signal()
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;p
ublic class Test26 {
private static final ReentrantLock lock = new ReentrantLock();
static Condition connection = lock.newCondition();
static boolean blog = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
while (!blog) {
try {
connection.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("1");
} finally {
lock.unlock();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
blog = true;
connection.signal();
System.out.println("2");
} finally {
lock.unlock();
}
}
}, "t2");
t2.start();
}
}
运行结果
4.变量的线程安全分析
- 互斥和同步的区别
互斥
是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码,比如使用synchronized或者Lock达到共享资源互斥效果同步
是由于线程执行的前后、顺序不同、需要一个线程等待其他线程运行到某个点,比如使用wait()¬ify()或者Lock的条件变量达到线程间通信的效果
- 成员变量和静态变量是否线程安全?
(1)如果没有共享,则线程安全
(2)如果被共享了,只有读操作,则线程安全。如果有读写操作,并且代码在临界区,则考虑线程安全问题 - 局部变量是否线程安全?
(1)局部变量是线程安全的
(2)局部变量引用的对象不一定:
①如果该对象没有逃离方法的作用范围,是线程安全的
②如果该方法逃离方法作用的范围,则需要考虑线程安全 - 常见线程安全类:
String
,Integer
,StringBuffer
,Random
,Hashtable
,java.util.concurrent包下的类
。他们的每个方法是原子的。但是他们组合起来使用不一定是原子的
举个例子:转账练习----给出现有读写操作的共享变量的类加锁(临界区的值)
import java.util.Random;
public class Test27 {
//转账数
static Random random = new Random();
public static int randomAccount(){
return random.nextInt(100) + 1;
}
//账户
private static class Account{
private int money;
public Account(int money){
this.money = money;
}
public int getMoney(){
return money;
}
public void setMoney(int money){
this.money = money;
}
//转账
public void transfer(Account target,int amount){
synchronized (Account.class){
if (this.money >= amount){
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
a.transfer(b,randomAccount());
}
});
t1.start();
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
b.transfer(a,randomAccount());
}
});
t2.start();
t1.join();
t2.join();
//查看转账2000次后的总金额
System.out.println(a.getMoney() + b.getMoney());
}
}
运行结果
5.线程的设计模式
1.同步模式
- 保护性暂停:当线程在访问某个对象时,发现条件不满足时,就暂时挂起等待条件满足时再次访问。
(1)产生的结果和消费的结果要一一对应
(2)有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
(3)如果有结果不断从一个线程到另一个线程那么可以使用消息队列
(4)JDK中,join的实现、Futrue的实现,采用的就是此模式
2.异步模式
- 生产者消费者
(1)产生的结果和消费的结果不需要要一一对应
(2)消费队列可以用来平衡生产和消费的线程资源
(3)生产者只负责生产数据,消费者只负责处理数据
(4)消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
(5)JDK中各种阻塞队列,采用的是这种模式
6.多把锁
多把不相干的锁。各干各的,互不干扰
- 在一把锁的情况下----效率太低,一个时间段只能干一件事
import java.util.Date;
public class Test28 {
private static class BigRoom {
//定义一个房间
private final Object Room = new Object();
private void sleep1() {
synchronized (Room) {
System.out.println(new Date() + "小南睡了2小时");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void study1() {
synchronized (Room) {
System.out.println(new Date() + "小女学习了2小时");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
bigRoom.sleep1();
}
}, "小南");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
bigRoom.study1();
}
}, "小女");
t2.start();
}
}
运行结果
- 多把锁的情况下----同一时间可以干多件事
import java.util.Date;
public class Test29 {
private static class BigRoom {
//定义两个房间
private final Object bedRoom = new Object();
private final Object studyRoom = new Object();
private void sleep1() {
synchronized (bedRoom) {
System.out.println(new Date() + "小南睡了2小时");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void study1() {
synchronized (studyRoom) {
System.out.println(new Date() + "小女学习了2小时");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
bigRoom.sleep1();
}
}, "小南");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
bigRoom.study1();
}
}, "小女");
t2.start();
}
}
运行结果
总结 |
---|
优点:增强并发度 |
缺点:如果一个线程需要同时获取多把锁,容易造成死锁 |
7.活跃度
1.死锁
在多线程编程中两个或两个以上的线程因为资源抢占而造成的线程无线等待的问题
public class Test30 {
public static void main(String[] args) {
//创建锁对象
Object A = new Object();
Object B = new Object();
//创建线程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
System.out.println("t1获取到锁A");
synchronized (B) {
System.out.println("t1获取到锁B");
}
}
}
},"t1");
//创建线程2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
System.out.println("t2获取到锁B");
synchronized (A) {
System.out.println("t2获取到锁A");
}
}
}
},"t2");
//启动线程
t1.start();
t2.start();
}
}
运行结果
- 死锁的查看
1.通过jconsole查看
2.通过Jstack查看
- 产生死锁的4个必要条件(必须同时成立)
(1)
互斥条件
:一个资源只能被一个线程持有,当被一个线程持有之后就不能被其他线程持有
(2)请求和保持条件
:当线程因请求资源而阻塞时,对已经获得的资源保持不放
(3)不剥夺条件
:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完由自己释放
(4)环路等待条件
:发生死锁时,多个线程在获取资源时形成了一个环形链
2.活锁
与死锁概念相反。在多线程编程中两个或两个以上的线程为了彼此间的响应而相互礼让,使没有一个线程能够继续前进
public class Test31 {
static volatile int count = 10;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(200);
count--;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count" + count);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (count < 20) {
try {
Thread.sleep(200);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count" + count);
}
}
}).start();
}
}
运行结果
3. 饥饿
一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束。
8.volatile关键字
1.volatile关键字原理
- 保证可见性
写屏障:保证在该屏障之前的,对共享变量的改动,都同步到主存中
读屏障:保证在该屏障之后,对共享变量的读取,加载的是主内存中最新的值- 保证有序性
写屏障:确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障:确保指令重排序时,不会将读屏障之后的代码排在读屏障之前- 无法保证原子性
2.volatile关键字的用法
public class Test32 {
//注意看这一句
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag){
}
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("设置flag为true");
flag = true;
}
});
t2.start();
}
}
运行结果
发现即使线程t2将flag = true;
,也不能被线程t1读写进去。线程无法停止执行。这就是所谓的内存可见性问题
。
下面将用volatile关键字解决
//加入volatile关键字
private static volatile boolean flag = false;
运行结果
3.synchronized和volatile比较
- volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;
- 从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁
- synchronized既能保证可见性,又能保证原子性,还可以保证有序性;而volatile只能保证可见性,无法保证原子性
9.乐观锁VS悲观锁
1.乐观锁
- 乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正
式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。并不总是能处理所有问题,所以会引入一定的系统复杂度。
应用场景:
- CAS机制
- ABA 问题
2.悲观锁
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会
上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。
应用场景
- synchronized锁机制
10.CAS机制
1.CAS(Compare And Swap)比较并替换
是一种无锁算法。实现的是Unsafe类,通过调用操作系统的
原子指令
来实现的
- CAS三个操作参数:
- 内存地址V
- 旧的预期值A
- 要修改的新值B
- CAS的执行流程
举个例子:
1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4.线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。
5.线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。
7.线程1进行交换,把地址V的值替换为B,也就是12.
假设你和你的朋友在转账
public class Test33 {
//注意看这一行代码
//设置初始值,相当于count = 0;
private static int count = 0;
//最大循环次数
private static final int MAXSIZE = 100000;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
count++;
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < MAXSIZE; i++) {
count--;//相当于i--
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println("结果:" + count);
}
}
运行结果
那你是不是觉得亏多了!!!
使用CAS算法就可以避免这样的问题
//设置初始值,相当于count = 0;
private static AtomicInteger count = new AtomicInteger(0);
运行结果
结果就正确了。。。但是如果在你刚给他转完100000,你的账户余额为0,但是这时候恰巧你的老板给你发工资了,也往你的账号上打了100000,这时候后台就无法判别你到底给你朋友转账没有。就出现了ABA问题。因为CAS算法对比的只是你的数值,而不追究地址,所以就会出现问题。
2.CAS存在的问题
- ABA问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
还是那个银行转账问题
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test34 {
//初始银行卡里有100
private static AtomicReference money = new AtomicReference(100);
public static void main(String[] args) throws InterruptedException {
//你给你的朋友转了100 剩余0
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//参数 100表示旧值,0表示新值
boolean result = money.compareAndSet(100,0);
System.out.println("第一次转出:" + result);
}
});
t1.start();
t1.join();
//你的老板给你打了100 剩余100
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//参数 0表示旧值,100表示新值
boolean result = money.compareAndSet(0,100);
System.out.println("转入:" + result);
}
});
t2.start();
t2.join();
//再一次给朋友转了100 剩余0
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
//参数 100表示旧值,0表示新值
boolean result = money.compareAndSet(100,0);
System.out.println("第二次转出:" + result);
}
});
t3.start();
t3.join();
}
}
运行结果
如果没有问题的话,第二次转出应该是false
解决方案
1.使用AtomicStampedReference ,给每次转账的操作加上版本号
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test35 {
private static AtomicStampedReference money = new AtomicStampedReference(100,1);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//参数 100代表旧值 0代表新值 1代表版本号
boolean result = money.compareAndSet(100,0,1,2);
System.out.println("第一次转出:" + result);
}
});
t1.start();
t1.join();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(0,100,2,3);
System.out.println("转入:" + result);
}
});
t2.start();
t2.join();
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0,1,2);
System.out.println("第二次转出:" + result);
}
});
t3.start();
t3.join();
}
}
运行结果
- 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
- 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
3.JUC(java util concurrent)包下的Atmoic类
- 原子整数:AtomicBoolean、atmoicInteger、AtmoicLong
- 原子引用:AtmoicReference(出现ABA问题)、AtmoicMarkableReference(只关心被修改过,不关心修改了几次)、AtmoicStampedReference(加版本号)
- 原子数组:AtmoicIntegerArray、AtmoicLongArray、AtmoicReferenceArray
- 字段更新器:AtmoicReferenceFileUpdater、atmoicIntegerFileUpdater、AtmoicLongFileUpdater
- 原子累加器:AtomicLong