提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- [1. 线程安全](#1. 线程安全)
-
- [1.1 死锁问题](#1.1 死锁问题)
-
- [1.1.1 一个线程一把锁](#1.1.1 一个线程一把锁)
- [1.1.2 两个线程两把锁](#1.1.2 两个线程两把锁)
- [1.1.3 N个线程N个锁](#1.1.3 N个线程N个锁)
- [2. volatile 关键字](#2. volatile 关键字)
- [3. wait等待 和 notify通知](#3. wait等待 和 notify通知)
- 总结
前言
1. 线程安全
sql
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count:" + count);
}

发现结果不是10000
而且每一次执行结果都不一样
因为t1和t2是并发执行的,这就是线程安全问题,或者叫做线程不安全


这个就是正确的顺序

这是其中一个错误
所以一定小于100000

这种错误,就可能导致小于50000
产生原因

原子就是不可分割的最小单位
count++就不是原子操作,有load,add,save等等操作
基本上很多操作多不是原子得到
但是java中的a=b这种操作是原子的

这两个也是线程不安全的原因
线程安全的解决方案
第一个原因,操作系统抢占式,我们是无法干预的
第二个原因不适用于所有情况,取决于实际需求,总不能要求不同时修改同一个变量吧,,,不太合理
第三个原因,就是把非原子的修改变为原子性的修改
-----》加锁

synchronized关键字
synch ro nized
sql
private static int count = 0;
private static Object object = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (object) {
count++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (object) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count:" + count);
}
大括号里面的代码,就是我们要打包成一个原子性的代码


如果一边加锁一边不加锁,还是一样的有线程安全问题
synchronized (object)加锁的是object,必须是对同一个对象加锁,所以是不同对象,还是线程不安全
锁对象必须是一个类对象
String,List这些都可以
但是int a =10;
a就不能作为锁对象

多个线程竞争锁是不可预期的
synchronized底层实现就是JVM中c++中实现的,在依靠操作系统中的api实现的加锁
,然后来自于cpu上的特殊的指令来实现的

StringBuffeer线程安全----》因为相关操作带有synchronized关键字,但是使用不当,也会有线程安全问题
Stringbuilder线程不安全

如果还定义一个解锁操作的话,那么如果中间return了,或者抛出异常;1,那么就解锁不了
但是synchronized无论是return还是异常,都是会自动解锁的


类对象就是Class,反射
sql
synchronized (object.getClass()) {
count++;
}
这样也是可以的
Class这种类对象也是唯一的
类对象也是对象,都可以写到synchronized
synchronized还可以修饰方法
sql
class Counter{
public int count = 0;
public void add(){
count++;
}
}
sql
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count:" + counter.count);
}

sql
synchronized (counter) {
counter.add();
}
可以这样加锁
sql
class Counter{
public int count = 0;
synchronized public void add(){
count++;
}
}
也可以这样加锁,都是等价的效果

synchronized 的锁对象就是this,就是counter
但是不要无脑加锁-------》使用锁可能会触发阻塞----》什么时候恢复呢--》不可预期
加锁是有代价的

比如这种static方法,没有this,你加锁的话,就相当于给类对象加锁
就相当于
给Counter.class加锁
若锁的是 类对象(比如 synchronized(Counter.class) 或静态同步方法):竞争范围是「整个类的所有实例」------ 因为每个类在 JVM 中只有 一个唯一的 Class 对象(存放在方法区),所有线程无论操作哪个实例,只要竞争这把锁,都会互斥。
简单说:synchronized(Counter.class) 锁定的是 Counter 类对应的 Class 对象,这把锁是「全局唯一」的,所有 Counter 实例(甚至无实例时)都会共享这把锁。
给类对象(静态方法)加锁就会阻塞所有的实例了
1.1 死锁问题
1.1.1 一个线程一把锁
sql
class Counter{
public int count = 0;
synchronized public void add(){
count++;
}
}
sql
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (counter) {
counter.add();
}
}
});
这就是死锁了
就是对一把锁,一个线程,加了两次锁

这样没有死锁,是因为java的synchronized的特殊处理
但是如果是c++的话,就可能会出问题
synchronized的特殊处理:可重入锁,这样就不会死锁了
可重入锁就是说额外记录一下,当前是哪个线程对这个线程加锁了
如果这次线程再次对这个锁进行访问,放行


synchronized就是可重入锁,然后引入一个计数器,看看真加锁几次,假加锁几次,这样就可以知道在什么地方,合适的解锁了
1.1.2 两个线程两把锁

sql
Thread t1 = new Thread(()->{
synchronized (lock1) {
System.out.println("lock1 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2) {
System.out.println("lock2 ");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock2) {
System.out.println("lock2 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock1) {
System.out.println("lock1 ");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();

现在它们两个线程的状态都是BLACKED的
都在获取下一个锁的时候阻塞了
1.1.3 N个线程N个锁

哲学家就餐问题
死锁的必要条件,四个条件缺一不可
1.锁是互斥的,锁的基本特性,你加锁了,别人就进不来---》不可改变
2.锁是不可抢占的,线程1拿到了锁,不主动释放的话,线程2就不能拿到锁,也是锁的基本特性--》不可改变
3.请求和保持,线程1拿到锁a之后,不释放a的前提下,去拿锁b
如果拿锁之前先释放自己的锁,就不会死锁了-
4.循环等待,多个线程获取锁的过程,存在循环等待
如何避免循环等待,给锁进行编号1,2,3,4,-----
加锁的时候,必须按照顺序加锁,必须先加锁编号小的,在加锁编号大的
就是一个线程有两个锁的时候,只能先对编号小的进行加锁,然后才对大的加锁,如果编号小的被别人用的,就只能一个锁都不用,就等待--》不会循环等待了---》不会在死锁了
sql
private static Object lock1 = new Object();
private static Object lock2 = new Object();
比如这两个锁,我们就约定,不管什么情况。都先对lock1 进行加锁,在对lock2进行加锁
sql
Thread t1 = new Thread(()->{
synchronized (lock1) {
System.out.println("lock1 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2) {
System.out.println("lock2 ");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock1) {
System.out.println("lock1 ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2) {
System.out.println("lock2 ");
}
}
});
大家都这样加锁,就不会死锁了

java中的集合类,大多数都是线程不安全的
多个线程修改同一个集合类的数据---》不安全
String
Vector
Stack
HashTable
StringBuffer都是加锁了的,安全的
ArrayList,Queue,HashMap,TreeMap,LinkedList,PriorityQueue都是不安全的
2. volatile 关键字
线程安全的第四个原因:内存可见性
sql
private static int n = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (n==0){
}
System.out.println("hello t1");
});
Thread t2 = new Thread(()->{
Scanner sc = new Scanner(System.in);
System.out.println("请输入:");
n = sc.nextInt();
});
t1.start();
t2.start();
t1.join();
t2.join();
}

但是输入了2,什么都没有变化
为什么呢
这就是内存可见性问题
就是一个线程读,一个线程写数据的时候就会出现这个问题
sql
while (n==0){
}
这个循环非常快,n==0这个判断有两件事




这个就是内存可见性问题


编译器的优化我们都是不好判断的,而且这是由写java的人来决定的
sql
private static int n = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (n==0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("hello t1");
});
Thread t2 = new Thread(()->{
Scanner sc = new Scanner(System.in);
System.out.println("请输入:");
n = sc.nextInt();
});
t1.start();
t2.start();
t1.join();
t2.join();
}

但是如果在循环里面加上休眠---》内存可见性问题就消失了,成功了
---》读取n内存数据的优化操作没了--》因为与读取内存相比,sleep开销更大,所以就不弄这个优化了
如果没有sleep,但还是希望能够没有bug--》volatile

sql
private static volatile int n = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (n==0){
}
System.out.println("hello t1");
});
Thread t2 = new Thread(()->{
Scanner sc = new Scanner(System.in);
System.out.println("请输入:");
n = sc.nextInt();
});
t1.start();
t2.start();
t1.join();
t2.join();
}


但是volatile不能解决原子性问题
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原⼦性, volatile 保证的是内存可⻅性
sql
private static volatile int n = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
n++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
n++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n=" + n);
}

3. wait等待 和 notify通知
因为线程的调度是随机的,所以有了这两个
多个线程---》控制线程之间执行某个逻辑的先后顺序
可以让后执行的逻辑,使用wait,先执行的线程完成某些逻辑之后,用notify来唤醒对应的wait
这两个还可以解决线程饿死问题
线程饿死:就是一直等待别人解锁,等半天
或者他刚刚解锁,然后上锁,让别人一直没有机会上锁
wait() / wait(long timeout): 让当前线程进⼊等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程
wait和notify是Object提供的方法,所以所有对象都可以wait和notify
sql
Object object = new Object();
System.out.println("wait前");
object.wait();
System.out.println("wait后");

IllegalMonitorStateException:表示锁状态非法
因为wait中会针对obj进行解锁,所以一定要先对obj进行加锁,才可以解锁
sql
Object object = new Object();
System.out.println("wait前");
synchronized (object) {
object.wait();
}
System.out.println("wait后");

但是没有打印出wait后呢---》阻塞到object.wait();了,由于代码中没有notify,所以会一直wait等待下去
所以wait的作用就是解锁,然后一直在这个方法这里阻塞等待,等待收到通知,这两个操作同时执行,是原子的,如果不是原子的,那么刚刚解锁,还没开始等待,别人突然就notify了---》自己再去等待,就可能不会被唤醒了
notify就是通知wait的线程被唤醒,被唤醒的线程就会重新竞争锁,再去继续执行原来得到操作
wait:释放锁,阻塞等待,收到通知之后,重新获取锁,继续执行
而且锁必须是同一个对象,才可以唤醒
sql
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lock) {
System.out.println("t1 wait 前 ");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 后 ");
}
});
Thread t2 = new Thread(()->{
System.out.println("t2 notify 前");
Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
lock.notify();
System.out.println("t2 notify 后");
});
t1.start();
t2.start();
t1.join();
t2.join();
}


注意notify使用的时候也要加锁
sql
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized (lock) {
System.out.println("t1 wait 前 ");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 后 ");
}
});
Thread t2 = new Thread(()->{
System.out.println("t2 notify 前");
Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
synchronized (lock) {
lock.notify();
}
System.out.println("t2 notify 后");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
因为如果锁都没被wait,没有解锁--》你去notify是不科学的


所以wait就相当于中途离开锁
让notify的线程进来,在notify通知前一个进来


如果有多个wait的--》notify的时候,就是会随机唤醒一个
sql
Thread t1 = new Thread(()->{
synchronized (lock) {
System.out.println("t1 wait 前 ");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 后 ");
}
});
Thread t2 = new Thread(()->{
synchronized (lock) {
System.out.println("t2 wait 前 ");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2 wait 后 ");
}
});
Thread t3 = new Thread(()->{
synchronized (lock) {
System.out.println("t3 wait 前 ");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t3 wait 后 ");
}
});
Thread t4 = new Thread(()->{
System.out.println("t4 notify 前");
Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
synchronized (lock) {
lock.notify();
}
System.out.println("t4 notify 后");
});
t1.start();
t2.start();
t3.start();
t4.start();

botifyAll就是唤醒所有了
sql
Thread t4 = new Thread(()->{
System.out.println("t4 notify 前");
Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
synchronized (lock) {
lock.notifyAll();
}
System.out.println("t4 notify 后");
});

notify是随机唤醒一个
没人wait,多次notify---》不会咋样,就和notify一次是一样的效果

sql
Thread t1 = new Thread(()->{
System.out.println("A");
synchronized (lock1) {
lock1.notify();
}
});
Thread t2 = new Thread(()->{
synchronized (lock1) {
try {
lock1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("B");
synchronized (lock2) {
lock2.notify();
}
});
Thread t3 = new Thread(()->{
synchronized (lock2) {
try {
lock2.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("C");
});
t1.start();
t2.start();
t3.start();

运行的时候,t2和t3先等待----》t1在notify,没有问题

但是如果这样呢,先notify了,t2再去wait---》wait晚了,没有通知了
sql
Thread t1 = new Thread(()->{
System.out.println("A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock1) {
lock1.notify();
}
});
这样就好了