1.线程不安全分析
(1)线程不安全的主要原因就是,系统的抢占式执行,对于内核设计者来说,这是非常方便的一个执行方式,但是这却却导致线程不安全的问题,也有不抢占执行的系统,但是这种系统会导致你的系统性能不是特别好。
(2)多个线程执行操作一个对象不会发生线程不安全的情况
(3)String之所以设置成无法改变,也是为了线程安全,如果想改可以试一试反射
(4)为了更好的解决线程不安全问题,我们可以将非原子操作改成原子操作这个月就可以更安全的保证线程的安全
2.锁
2.1锁的定义
(1)锁可以理解成,解锁,上锁这个操作,拿到锁以后,就可以使你的线程互斥,只有这个线程搞完然后释放锁,然后需要的线程拿到锁以后才能继续执行后序的操作。
(2)在一个程序中可以有多把锁,只有对一个非原子事物进行加锁才会发生互斥这个情况
图解
2.2锁的简单总结
2.3锁的具体实现(代码)
(1)创建一个类型(Object)由于Object类是所有类的父类所以所有类都可以拿到以Object枷锁的锁。(这种设定是不太合理的)
在python中,以及c++中能够加锁的是非常少的。
(2)锁的格式
这个锁是同步的,同步的反义词就是独占
(3)锁的注意事项
(4)在使用sychronized方法,进入到了大括号中,就是加锁了,当这个大括号中的运行结束以后,就会自动的解锁(其他的语言也是这样的)
2.4锁的互斥
如果两个线程对对同一个对象进行加锁就会发生互斥,对两个不同的对象进行加锁就不会发生互斥。
(1)代码(互斥)
这种情况就是两个线程同时用一把锁对线程进行加锁,这时候就会发生互斥,程序会直接终止
(2)代码(不互斥)
用两把锁对其进行加锁这时候就可以避免互斥了
2.5图集锁的互斥
这个拿到锁的过程可以理解为我约会一个女生这个女生同意了这个时候女生就是被我上锁了,这时候要是其他男生来就会被阻塞,约会不了
2.6代码实例
(1)代码
package thread;
class Counter {
private int count = 0;
synchronized public void add() {
count++;
}
public static void func() {
synchronized (Counter.class) {
// ....
}
}
public int get() {
return count;
}
}
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Counter counter2 = new Counter();
// Object locker = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// counter.add();
counter.func();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// counter2.add();
counter2.func();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter.get());
}
}
我们可以用counter这个类来进行加锁,对于java来说锁只是一个标准,只要加锁的对象是一样的就行了。
其中我们也可以用this来进行加锁,这个方法也是不错的相当于简化()直接在括号里面写一个this。
3.锁的扩展用法
(1)针对类的类对象进行加锁
(2)通过反射来拿到当作锁的类的信息并进行加锁,类似于加锁继承
其中我们要注意的就是如果在多个线程对这个func方法进行调用这时候就会发生锁竞争。
(3)解决方式 加锁这个func中的方法,当这个锁使用这个方法的时候相当于是this这个对象在使用func中的方法的这个对象,这时候就不会发生锁的竞争。
2.5锁的嵌套
(4)对对象加锁使用对象中的方法发生锁竞争的情况。
(5)我们也可以使用静态方法来进行加锁实现,但是我并不是很推荐
4.锁的要记住的东西
3.死锁问题的出现?
3.1小练习来判断这个代码是否有问题(可能有问题也是算的)
(1)
这种写法也会可能出现问题,两个线程可能发生的问题有,t1执行完毕了,但是t2还在执行,这时候就会变成串行执行
(2)
这串代码乍一眼看上去是没毛病,但是其实他是有问题的,for循环中的i的值会被锁锁住,所以可能会出现下面的情况,{t1在执行的过程中,t2已经执行完毕了这时候t2的i就会把t1的i给覆盖这时候就会出现问题}
(3)特殊情况(可重入锁)
3.2synchronized为什么这么智能不会被死锁?
(1)
当一个线程已经获得了锁并再次请求同一个锁时,JVM不会立即释放第一个锁再重新获取。相反,它会继续持有这个锁,而不会导致阻塞。这种情况被称为重入锁。因此,即使同一个线程多次请求同一个锁,它也不会释放锁再重新获取,而是继续持有锁。
另一方面,如果一个线程尝试获取一个已经被其他线程持有的锁,那么它将被阻塞,直到锁可用。这种行为是由JVM的线程调度器和对象监视器来管理的。
3.3可重入锁的作用
可重入锁相当于保护你的这个线程的安全如果用了这个线程以外的锁就会将这个锁进行阻塞,这样可以更好的保证你的线程安全
4.死锁会出现的3个场景
(1)
(2)场景而就是有两个线程,两把不同的锁,两个线程分别需要另一个线程解锁释放的锁才能执行下一步这时候就会发生死锁
(3)第三个场景就是第二个场景的升级版本
代码
5.小知识
1.如果需要查看哪个线程需要哪一把锁我们可以用idea中自带的软件来查找这个线程是持有的哪一把锁。画圆圈的就是这些线程获取到的锁以及这个线程的状态是什么也可以找到。
2.死锁发生的条件必须要背下来的小知识(必须要背下来)
3.多个线程要用锁的时候只需要将所得顺序约定好然后挨个加锁就行了(大致提一下后序会详细讲解)
5.引起线程不安全的主要原因
(1)内存的可见性是可能使得线程发生线程的不安全问题。
接下来我会写一段代码来对这种情况来进行详细的讲解。
两个线程分别是用来读和写的,但是我们会发现输入的是0但是却没有将线程1给停止。
接下来我会用图解的形式给大家详细的讲解
(2)首先我要先给大家讲解一个东西,当一个计算机对一个数据进行读取的或者比较的时候是三个操作的。
(3)load先从内存中读取数据到cpu寄存器中
(4)cmp(比较,同时会产生跳转)条件成立的时候就会继续顺序执行,条件不成立的时候就会跳转到另一个地址上面来进行执行。
(5)其中我们要注意在循环中这种操作的速度是非常快的,短时间会出现大量的load和cmp反复执行的效果
而且load执行消耗的时间会比cmp多很多!!
多个几千倍,上万倍!!
(6)在上面代码的过程中,load的速度非常慢,执行一次load消耗的时间顶成千上万倍的load的次数
(7)另外JVM还会每次发现load的执行结果是一样的,(为空),这时候JVM就会直接把load的操作直接优化了,读取到的结果将会是空。所以这就会使得循环不会被终止(相当于裁员)
(8)其中要注意的就是IO操作注定是反复hi下的结果是不相同的,所以IO操作是不会被优化掉的,IO操作是在load然后cmp之后的操作。
4.解决方式
关键字(volatile)只要用这个关键字进行修饰就让JVM知道这一部分是不需要进行优化的。
(有些人想既然不优化的化不会发生这种事情那么,JVM干嘛要进行优化,给大家举一个简单的例子,如果进行优化了,那么这个线程将会比以前的速度快十倍多)
6.线程饿死wait关键字
我们用wait关键字就代表着