死锁
想获取到第二把锁,就需要执行完第一层大括号,想要执行完第一层大括号,就要先获取到第二层的锁。
synchronized (counter2){
synchronized (counter2){
}
}
例子:t2先启动,t2进行加锁后一定成功,但是如果t2进行二次加锁的时候因为counter2已经被锁定了,所以他需要外层大括号的counter2进行解锁,但是这又是加锁操作,所以就会一直阻塞等待,于是就矛盾了,产生了对峙的画面(狗咬狗不松口)。
"引用计数"
可重入锁,防止程序员搞成死锁。
如何判定,当前遇到的}是最外层的}??JVM 是咋知道的??
更简单的办法,就是给锁对象里也维护一个计数器
每次{n++,每次遇到},n--。
就相当于当自己的锁给自己的锁加锁的时候就会形成嵌套锁的时候,就会防止形成嵌套锁的情况,
synchronized不存在问题,idea没必要提示~~
死锁的场景
场景一:锁是不可重入锁,并且一个线程针对一个锁对象,连续加锁两次通过引入可重入锁,问题就迎刃而解了,九月场景一是锁不住的对吧
场景二:两个线程两把锁
有线程1和线程2,以及有锁A和锁B
现在,线程1和2 都需要获取到锁A和锁B
拿到锁A之后,不释放A,继续获取锁B
先让两个线程分别拿到一把锁,然后在去尝试获取对方的锁
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Object o1= new Object();
Object o2= new Object();
Thread thread1=new Thread(()->{
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o2){
System.out.println("两把锁");
}
}
});
Thread thread2=new Thread(()->{
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o1){
System.out.println("两把锁");
}
}
});
thread2.start();
thread1.start();
thread1.join();
thread2.join();
System.out.println("结束");
}
}
这会产生死锁状态。
场景三:N个线程,M把锁
死锁的四个必要条件 !!!
1.锁具有互斥特性.(基本特点,一个线程拿到锁之后,其他线程就得阻塞等待)
2.锁不可抢占(不可被剥夺) 一个线程拿到锁之后,除非他自己主动释放锁,否则别人抢不走~~
3. 请求和保持,一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁!
4.循环等待. 多个线程获取多个锁的过程中,出现了循环等待. A 等待 B,B 又等待 A.
必要条件:缺一不可任何一个死锁的场景,都必须同时具备上述四点只要缺少一个,都不会构成死锁
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Object o1= new Object();
Object o2= new Object();
Thread thread1=new Thread(()->{
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o2){
System.out.println("两把锁");
}
}
});
Thread thread2=new Thread(()->{
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o2){
System.out.println("两把锁");
}
}
});
thread2.start();
thread1.start();
thread1.join();
thread2.join();
System.out.println("结束");
}
}
约定每个哲学家,必须先获取编号小的筷子,后获取编号大的筷子~
内存可见性
import java.util.Scanner;
public class Test3 {
public static int count=0;
public static void main(String[] args) {
Thread thread=new Thread(()->{
while (count==0){
}
System.out.println("t1 执行结束");
});
Thread thread1=new Thread(()->{
Scanner scanner=new Scanner(System.in);
count=scanner.nextInt();
});
thread1.start();
thread.start();
}
}
import java.util.Scanner;
public class Test3 {
public static int count=0;
public static void main(String[] args) {
Thread thread=new Thread(()->{
while (count==0){
}
});
Thread thread1=new Thread(()->{
Scanner scanner=new Scanner(System.in);
count=scanner.nextInt();
});
thread1.start();
thread.start();
}
}
代码转换为下面的时候就会一直进入无限循环,不能跳出循环。
由于系统自带简化
while (count==0){ } 会load从内存读取数据到cpu寄存器cmp(比较,同时会产生跳转)条件成立,继续顺序执行条件不成立,就跳转到另外一个地址来执行。
但是循环过快的时候,load循环速度慢,执行load的时间使上万次的cmp执行效率。
所以就把load优化掉了,专业昂就不会进行判断,这样就会使效率提高
上述问题本质上还是编译器优化引起的.优化掉load操作之后,使t2线程的修改,没有被t1线程感知到“内存可见性”问题
volatile
是告诉编译器,不要触发上述优化
如何解决上述内存可见性问题??就内存可见性问题来说,可以通过特殊的方式来控制,不让它触发优化的volatile关键字。
volatile是专门针对内存可见性的场景来解决问题的,并不能解决之前,两个线程循环count++的问题
引l入synchronized其实是因为加锁操作本身太重量了.相比于load来说,开销更大,编译器自然就不会对load优化了.(和加上sleep/io操作)
当t1执行的时候,要从工作内存中读取count的值,而不是从主内存中.后续t2修改count,也是会先修改工作内存,同步拷贝到主内存.但是由于t1没有重新读取主内存,导致最终t1没有感知到t2的修改.
线程等待通知机制
系统内部,线程是抢占式执行,随机调度.程序员也是有手段干预的.通过“等待”的方式,能够让线程一定程度的按照咱们预期的顺序来执行。
例子:在ATM机取钱的时候,如果1号去取钱,但是ATM正好没钱,这时候2号再进去取钱,就会产生频繁的没有意义的系统调度,cpu永远在做无效的工作,就会影响效率。
这时候就需要应用线程等待的方法,查看当前的逻辑是否能执行,如果不能执行就主动wait,避免造成无效工作,等待后续时机成熟(ATM有人存钱了),阻塞就会自动被唤醒
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
wait使先解锁然后再阻塞等待,其他的线程就可以获取到object这个锁,防止了死锁的产生。
import java.util.Scanner;
public class Test4 {
public static void main(String[] args) {
Object o=new Object();
Thread thread=new Thread(()->{
synchronized (o){
try {
o.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread thread1=new Thread(()->{
Scanner scanner=new Scanner(System.in);
synchronized (o) {
scanner.nextInt();
o.notify();
}
});
thread.start();
thread1.start();
}
}
先解除再堵塞等待,可以通过notiify来唤醒。
唤醒后不会立刻执行从WAITING-----RUNNABLE---BLOCKED
但是没有规定执行顺序的时候很有可能会导致,thread1先执行,最后导致notify产生不了效果
o.notifyAll();//唤醒全部等待的线程
多线程代码
1.单例模式
单例模式是一个经典的设计模式。
单例模式--》单个实例,instance就是对象。
整个过程中的某个类,有且只有一个对象(不会再new出来新的对象)
饿汉模式
只要运行就会立刻instance,无论后面用不用都会调用
public class Singleton {
private static Singleton instance=new Singleton();//只要启动就会立刻生成给instance这个对象
public static Singleton getInstance(){//就可以通过getlnstance来获取已经new好的这个而不是重新new
return instance;
}
//要禁止外部代码来创建该类的实例~~
private Singleton(){
//类之外的代码,尝试new的时候,,势必就要调用构造方法由于构造方法私有的.无法调用,就会编译出错!!
}
}
懒汉模式
计算机中,谈到懒,往往是一个"褒义词",而且是“高效率”的代表
懒汉模式,不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建(如果不使用了,就会把创建实例的代价就节省下来了)
public class SingletonLazy {
private static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if (instance==null){
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
//重中之重
}
}
在只考虑一个方法getInsttance方法的情况下考虑:
饿汉模式是否安全?安全=>1
创建实例的时机是在java进程启动(比main调用还早的时机)
懒汉模式是否安全?不安全
t2切换回来之后还是要进行新的new对象操作,就会产生多个对象了。
Instance中的地址指向的那个对象,,可就是一个大的对象了。
产生的对象会被覆盖但是产生以及浪费的时间可是真金白银。
添加
synchronized锁操作。
public class SingletonLazy {
public static Object object = new Object();
public static void main(String[] args) {
}
private static SingletonLazy instance = null;//只要启动就会立刻生成给instance这个对象
public static SingletonLazy getInstance() {//就可以通过getlnstance来获取已经new好的这个而不是重新new
if (instance == null) {
synchronized (object) {
instance = new SingletonLazy();
}
return instance;
}
//要禁止外部代码来创建该类的实例~~
private SingletonLazy() {
//类之外的代码,尝试new的时候,,势必就要调用构造方法由于构造方法私有的.无法调用,就会编译出错!!
}
}
}
public static SingletonLazy getInstance() {//就可以通过getlnstance来获取已经new好的这个而不是重新new
synchronized (object) {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
针对后续调用,明明没有线程安全问题,还要加锁,就是画蛇添足加锁本身,也是有开销的=>可能会使线程阻塞)
所以要进行优化操作。
StringBuilder 不带锁
StringBuffer 带锁
public static SingletonLazy getInstance() {
if (instance==null) {//判定是否要加锁实例化之后,线程自然安全了,就无需加锁了实例化之前,new之前,就应该要加锁
synchronized (object) {//在这俩if之间,synchronized会使该线程阻塞,阻塞过程中其他线程就可能会修改Instance的值
if (instance == null) {//判定是否要创建对象
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
}
此外还要加上volatile这样防止他进行优化操作。
t1线程修改了Instance引l用,t2有可能读不到.(概率应该是比较小).加上volatile主要是为了万无一失.
指令重排序
加了volatile也能够解决指令重排序引l起的线程安全问题
调整顺序最主要的目的就是提高效率.(前提是保证逻辑是等价的)
重排序的前提,一定是重新排序之后,逻辑和之前等价单线程下,编译器进行指令重排序的操作,一般都是没问题的.编译器可以准确的识别出,哪些操作可以重排序,而不会影响到逻辑~~
instance=new SingletonLazy
这一行代码,其实还可以简要细分成三个步骤~
例子
1.买了个房子)2.装修3.拿到钥匙
123(精装房,开发商直接给你装修好,你收房的时候,已经装修完了
132(毛坏房,自己装修)
1.申请内存空间
2.调用构造方法.(对内存空间进行初始化)
3.把此时内存空间的地址,赋值给Instance引用
在指令重排序优化策略下,上述执行的过程不一定是123也可能是132(1一定是先执行的)
如果这样执行1.申请内存3把地址赋值给引用
一旦执行完意味着Instance就非null !!但是指向的对象其实是一个未初始化的对象(里面的成员都是0)这样就会返回没有初始化的对象。
如果在执行其他方法的时候就会出现没有初始化的对象在操作会产生非常严重的问题/
2.调用方法
所以要引用volatile
按照加上volatile之后,此时,t2线程读到的数据,一定是t1已经构造完毕的完整对象了.(一定是123都执行完毕的对象)
public class SingletonLazy {
private static volatile SingletonLazy instance = null;//3
public static Object object = new Object();
public static SingletonLazy getInstance() {
if (instance == null) {//2
synchronized (object) {//1
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
2.阻塞队列
0.普通队列线程不安全的
1.优先级队列.
2.阻塞队列先进先出,线程安全,并且带有阻塞功能-------》1.队列为空,尝试出队列,出队列操作就会阻塞一直阻塞到队列不空为止2.队列为满,尝试入队列,入队列操作也会阻塞一直阻塞到队列不满为止.
3.消息队列不是普通的先进先出,而是通过topic(一块)这样的参数来对数据进行归类出队列的时候,指定topic,每个topic下的数据是先进先出的.
生产者消费者模型
在开发中主要又有两方面的意义
1.能够让程序进行解耦
举个包饺子的例子:
中间的盖帘,相当于一个阻塞队列/消息队列
如果师娘擀的慢,我俩包的快,此时,盖帘上就空着了我和小汤就会阻塞等待如果我俩包的慢,师娘擀的快~~很快盖帘放满了师娘就要阻塞等待。
如果让A直接调用B意味着A的代码中就要包含很多和B相关的逻辑B的代码中也会包含和A相关的逻辑彼此之间就有了一定的耦合。
一旦对A做出修改,可能就会影响到B反之亦然一旦A出bug,也容易把B牵连到反之也是亦然。
站在A的视角,不知道B的存在,只关心和队列的交互站在B的视角,不知道A的存在,只关心和队列的交互此时,对A的修改,就不太容易影响到BA如果挂了,也不会影响到B。
2.能够使程序"削峰填谷"
客户端发来的请求,个数多少,没法提前预知.遇到某些突发事件,就可能会导致客户端给服务器的请求激增~~
如果是这样的话,b要进行许多重量级操作,一旦A收到的请求增加了,B的请求也会增加,A做的工作简单,B做的复杂,这样B就容易崩溃。
增加了mq后无论A给队列写多快,B都可以按照固有的节奏来消费数据B的节奏,就不一定完全跟着A了.相当于队列把B保护起来了。
阻塞队列,
BlockingQueue<E>
阻塞队列只需要考虑,入队列和出队列即可.阻塞队列没有“取队首元素”操作.(也不是完全没有,只不过是没有阻塞功能)
其中的offer和put没有阻塞功能,但是put和take有