【多线程】synchronized和 volatile

目录

1.synchronized的基本使用

2.监视器锁monitor lock

2.1synchronized 的特性

 2.2死锁

 3.Java 标准库中的线程安全类

4. volatile 关键字

5.wait 和 notify

5.1wait()方法

 5.2notify()方法


1.synchronized的基本使用

synchronized 的本质操作,就是修改了Object 对象中的 对象头 里的一个标记

当两个线程同时针对一个对象加锁,才会产生竞争

如果两个线程针对不同对象加锁,就不会竞争。

举个例子:同学A 进一个ATM取款, 同学B 也想要进这个ATM取款,就得等A出来;

如果 同学B 想要进另一个ATM取款,就不需要等

(1)把synchronized 加到普通方法上

直接修饰普通方法(实例方法),也就是相当于把锁对象指定为 this

synchnorized public void increase() {
    count++;
}

(2)把synchronized加到代码块上

如果要是针对某个代码块加锁,就需要手动指定。

public void increase() {
    synchnorized (this) {

        count++;
    }

}

(3)synchronized 加到代码块上

把synchronized 加到静态方法上,严谨的说是“类方法”。

public void increase() {
    synchnorized (Counter.class) {

      
    }

}

2.监视器锁monitor lock

2.1synchronized 的特性

 (1)互斥

synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到同一个对象 synchronized 就会 阻塞等待 .
进入 synchronized 修饰的代码块 , 相当于 加锁
退出 synchronized 修饰的代码块 , 相当于 解锁

 (2)刷新内存

synchronized的工作过程

1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
(3)可重入
可重入就是同一个线程,连续加锁两次。如果出现死锁,就是不可重入,不死锁就是可重入。
如下,先在外层加了异常锁,里层又对同一个对象在加一次锁。
外层锁:进入方法,则开始加锁.这次能够加锁成功.当前锁是没有人占用的.
里层锁:进入代码块,开始加锁,这次加锁不能加锁成功. (按照咱们之前的观点来分析) ,因为锁被外层占用着,得等外层锁释放了之后,里层锁才能加锁成功~
外层锁要执行完整个方法,才能释放~~
但是要想执行完整个方法,就得让里层锁加锁成功继续往下走~~
synchnorized public void increase() {
    synchnorized (this) {

        count++;
    }

}

对于可重入锁来说,上述连续加锁操作,不会导致死锁。

可重入锁内部,会记录当前的锁被哪个线程占用的,同时也会记录一个“加锁次数"
线程a针对锁第一次加锁的时候,显然能够加锁成功,锁内部就记录了当前的占用着是a,同时加锁次数为1.
后续再a对锁进行加锁,此时就不是真加锁,而是单纯的把计数给自增,加锁次数为2~
后续再解锁的时候,先把计数进行-1.当锁的计数减到0的时候,就真的解锁。

可重入锁的意义就是降低了程序猿的负担. (使用成本,提高了开发效率)
但是也带来了代价,程序中需要有更高的开销(维护锁属于哪个线程,并且加减计数.降低了运行效率)

 2.2死锁

1.一个线程一把锁

2.两个线程两把锁

3.N个线程,M把锁

死锁的四个必要条件

1.互斥使用~一个锁被一一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)
2.不可抢占- 一个锁被一个线程占用了之后,其他的线程不能把这个锁给抢走. (挖墙脚是不行的)
3.请求和保持当-个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是被该线程持有的~
4.环路等待等待关系, 成环了~~A等B,B等C,C又等A

 3.Java 标准库中的线程安全类

 Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

 但是还有一些是线程安全的. 使用了一些锁机制来控制

Vector ( 不推荐使用 )
HashTable ( 不推荐使用 )
ConcurrentHashMap
StringBuffer

4. volatile 关键字

volatile 能保证内存可见性,禁止编译器优化

 volatile只是保证可见性,不保证原子性

volatile 不会引起线程阻塞

5.wait notify

由于线程之间是抢占式执行的 , 因此线程之间执行的先后顺序难以预知 .
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

 wait() / wait(long timeout): 让当前线程进入等待状态.

notify() / notifyAll(): 唤醒在当前对象上等待的线程 .

5.1wait()方法

使当前执行代码的线程进行等待 . ( 把线程放到等待队列中 ) ,释放当前的锁
满足一定条件时被唤醒 , 重新尝试获取这个锁 .
调用wait 方法的线程,就会陷入阻塞,阻塞到其他线程通过 notify 来通知
public class Tes16 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized(object) {
            System.out.println("wait 前");
            object.wait();//等待
            System.out.println("wait 后");
 

这样在执行到 object.wait() 之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()

 5.2notify()方法

notify 方法是唤醒等待的线程 .
方法 notify() 也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify ,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。 ( 并没有 " 先来后到 ")在notify() 方法后,当前线程不会马上释放该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

 如下通过线程 t2 唤醒 线程 t1 的wait


public class Test17 {
    private static Object locker = new Object();
    public static void main(String[] args) throws InterruptedException {
        //创建线程 t1
        Thread t1 = new Thread(() -> {
            // 进行 wait
            synchronized(locker) {
                System.out.println("wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 之后");
            }
        });
        t1.start();

        Thread.sleep(3000);//main线程阻塞3秒

        //创建线程 t2
        Thread t2 = new Thread(() -> {
            synchronized(locker) {
                System.out.println("notify 之前");
                locker.notify();
            }
        });
        t2.start();
        System.out.println("notify 之后");
    }
}

猜你喜欢

转载自blog.csdn.net/m0_60494863/article/details/124770401