Java--多线程之synchronized和lock;死锁(四)

一、synchronized

在了解synchronized之前,我们先看一个线程不安全的示例

如一个账户中有1万块钱,两个人同时取钱,会导致余额不对,或者取的钱比账户中金额还多

public class AccountThreadTest {
    public static void main(String[] args) {
        Account account = new Account("xiaoming",10000.0);

        AccountThread t1 = new AccountThread(account);
        AccountThread t2 = new AccountThread(account);

        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class AccountThread extends Thread{
    private Account account;

    public AccountThread(Account account){
        this.account = account;
    }

    @Override
    public void run() {
        double money = 5000.0;

        account.withDraw(money);

        System.out.println(Thread.currentThread().getName() + "在" + account.getAccount() + "账户成功取款" + money + ";余额为" + account.getBalance());
    }
}

class Account {
    private String account;
    private Double balance;

    public Account(){

    }

    public Account(String account,Double balance){
        this.account = account;
        this.balance = balance;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    //取钱
    public void withDraw(Double money){
        Double afterMoney = this.getBalance() - money;

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.setBalance(afterMoney);
    }
}

输出如下

t2在xiaoming账户成功取款5000.0;余额为5000.0
t1在xiaoming账户成功取款5000.0;余额为5000.0

t1和t2线程分别在同一个总金额为1万的账户取款 5千,然余额还是5千,这就导致了多线程并发下数据不安全的情况

(一)多线程并发的环境下安全问题

1、多线程并发数据安全问题

满足如下3个条件之后,就会存在线程安全问题

1、多线程并发

2、有共享数据

3、共享数据有修改的行为

2、解决多线程并发数据安全问题方法:线程同步机制

线程同步机制就是线程排队执行,线程排队执行就会牺牲一部分执行效率,但是数据安全才是第一位,数据安全才可以谈效率;数据不安全,效率无从谈起

在 Java 语言中,保证线程安全性的主要手段是加锁,而 Java 中的锁主要有两种:synchronized 和 Lock

(二)synchronized

synchronized是Java中的一个关键字,他的实现时基于jvm指令去实现的,在JDK1.5之前,我们在编写并发程序的时候无一例外都是使用synchronized来实现线程同步的,而synchronized在JDK1.5之前同步的开销较大效率较低,因此在JDK1.5之后,推出了代码层面的Lock接口(synchronized为jvm层面)来实现与synchronized同样功能的同步锁

首先看synchronized

synchronized官方解释如下

Java--多线程之synchronized和lock;死锁(四)_第1张图片

Synchronized同步方法可以支持使用一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的

简单就是说Synchronized的作用就是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保同一个时刻最多只有一个线程执行同步代码,从而保证多线程环境下并发安全的效果。 如果有一段代码被Synchronized所修饰,那么这段代码就会以原子的方式执行,当多个线程在执行这段代码的时候,它们是互斥的,不会相互干扰,不会同时执行

Synchronized工作机制是在多线程环境中使用一把锁,在第一个线程去执行的时候去获取这把锁才能执行,一旦获取就独占这把锁直到执行完毕或者在一定条件下才会去释放这把锁,在这把锁释放之前其他的线程只能阻塞等待

synchronized是Java中的关键字,被Java原生支持,是一种最基本的同步锁

synchronized修饰的对象有以下几种: 

1、修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象

2、修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象

3、修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象

4、修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象

1、修饰普通方法

/**
 * synchronized 修饰普通方法
 */
public synchronized void method() {
    // ....
}

synchronized 修饰普通方法时,被修饰的方法被称为同步方法,其作用范围是整个方法,作用的对象是调用这个方法的对象 this

2、修饰静态方法

/**
 * synchronized 修饰静态方法
 */
public static synchronized void staticMethod() {
    // .......
}

synchronized 修饰静态方法时,其作用范围是整个程序,这个锁对于所有调用这个锁的对象都是互斥的 

对于静态方法来说 synchronized 加锁是全局的,也就是整个程序运行期间,所有调用这个静态方法的对象都是互斥的,而普通方法是针对对象级别的,不同的对象对应着不同的锁

静态方法加锁是全局的,针对的是所有调用者;而普通方法加锁是对象级别的,不同的对象拥有的锁也不同

对象锁:1个对象1把锁,100个对象100把锁
类锁:100个对象,也可能只是1把类锁

3、修饰代码块

在日常开发中,最常用的是给代码块加锁,而不是给方法加锁,因为给方法加锁,相当于给整个方法全部加锁,这样的话锁的粒度就太大了,程序的执行性能就会受到影响,所以通常情况下,我们会使用 synchronized 给代码块加锁,它的实现语法如下

public void classMethod() throws InterruptedException {
    // 前置代码...
    
    // 加锁代码
    synchronized (SynchronizedUsage.class) {
        // ......
    }
    
    // 后置代码...
}

相比于修饰方法,修饰代码块需要自己手动指定加锁对象,加锁的对象通常使用 this 或 xxx.class 这样的形式来表示

// 加锁某个类
synchronized (SynchronizedUsage.class) {
    // ......
}

// 加锁当前类对象
synchronized (this) {
    // ......
}

this VS class

使用 synchronized 加锁 this 和 xxx.class 是完全不同的,当加锁 this 时,表示用当前的对象进行加锁,每个对象都对应了一把锁;而当使用 xxx.class 加锁时,表示使用某个类(而非类实例)来加锁,它是应用程序级别的,是全局生效的

可以从如下四个实例中区分

/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 不需要,因为doOther()方法没有synchronized
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        MyThread t1 = new MyThread(myClass);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    //synchronized 锁住 doSome
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

输出

doSome begin
doOther begin
doOther over
doSome over

/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 需要,doOther()方法有synchronized
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        MyThread t1 = new MyThread(myClass);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    //synchronized 锁住 doSome
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    //synchronized 锁住 doOther
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

输出

doSome begin
doSome over
doOther begin
doOther over
/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 不需要,因为MyClass对象是两个,两把锁
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass1 = new MyClass();
        MyClass myClass2 = new MyClass();

        MyThread t1 = new MyThread(myClass1);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass2);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    //synchronized 锁住 doSome
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    //synchronized 锁住 doOther
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

输出

doSome begin
doOther begin
doOther over
doSome over

/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass1 = new MyClass();
        MyClass myClass2 = new MyClass();

        MyThread t1 = new MyThread(myClass1);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass2);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    //synchronized static 锁住 doSome
    public synchronized static void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    //synchronized static 锁住 doOther
    public synchronized static void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

输出

doSome begin
doSome over
doOther begin
doOther over

上述取钱实例我们只需要在 Account 中的共享对象 加把锁即可

class Account {
    private String account;
    private Double balance;

    // 实例变量Account对象是多线程共享的,Account对象中的实例变量obj也是共享的)
    Object obj = new Object();

    public Account(){

    }

    public Account(String account,Double balance){
        this.account = account;
        this.balance = balance;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    /**
     线程同步机制的语法是:
     synchronized(){
        // 线程同步代码块
     }
     被synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问
     synchronized()小括号中传的“数据”必须是多线程共享的数据,才能达到多线程排队

     ()中参数
     假设t1、t2、t3、t4、t5,有5个线程
     希望t1 t2 t3排队,t4 t5不需要排队
     在()中写一个t1 t2 t3共享的对象,而这个对象对于t4 t5来说不是共享的

     synchronized()执行原理
     1、假设t1和t2线程并发,开始执行代码时,有一个先后顺序
     2、假设t1先执行,遇到了synchronized,t1自动找“共享对象”的对象锁,
     找到之后并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直
     占有这把锁的,直到同步代码块代码结束,这把锁才会释放
     3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有
     共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
     直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
     t2占有这把锁之后,进入同步代码块执行程序
     */

    //取钱
    public void withDraw(Double money){
//        Object obj2 = new Object();

//        synchronized (obj) {
        //synchronized ("abc") { // "abc"在字符串常量池当中,会被所有线程共享
        //synchronized (null) { // 报错:空指针
//        synchronized (obj2) { // obj2是局部变量,不是共享对象
        synchronized (this){
            Double afterMoney = this.getBalance() - money;

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            this.setBalance(afterMoney);
        }
    }
}

(三)synchronized()执行原理

1、假设t1和t2线程并发,开始执行代码时,有一个先后顺序

2、假设t1先执行,遇到了synchronized,t1自动找“共享对象”的对象锁, 找到之后并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直 占有这把锁,直到同步代码块代码结束,这把锁才会释放

3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有 共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束, 直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后 t2占有这把锁之后,进入同步代码块执行程序

二、lock

从JDK 5.0开始,Java提供了更强大的线程同步机制一通过 显式定义同步锁对象来实现同步。同步锁使用L ock对象充当

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具

锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁, 线程开始访问共享资源之前应先获得Lock对象

ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantL ock,可以显式加锁、释放锁

如下,模拟买票流程

public class LockTest {
    public static void main(String[] args) {
        BuyTicketThread buyTicketThread = new BuyTicketThread();

        Thread t1 = new Thread(buyTicketThread);
        Thread t2 = new Thread(buyTicketThread);
        Thread t3 = new Thread(buyTicketThread);

        t1.setName("小明");
        t2.setName("小李");
        t3.setName("小张");

        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTicketThread implements Runnable{
    private int ticketNum = 10;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    //synchronized 同步方法,锁的是this
    private /*synchronized*/ void buy() {
        if (ticketNum <= 0){
            flag = false;
            return;
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "购买了第" + ticketNum-- + "张票");
    }
}

输出

小李购买了第9张票
小张购买了第10张票
小明购买了第8张票
小张购买了第5张票
小明购买了第6张票
小李购买了第7张票
小张购买了第4张票
小明购买了第2张票
小李购买了第3张票
小张购买了第0张票
小明购买了第-1张票
小李购买了第1张票

如果不加锁,会导致多个不同线程买到同一张票,甚至0和负数 ;如果在买票方法中加入synchronized修饰即可保证买票正常

import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    public static void main(String[] args) {
        BuyTicketThread buyTicketThread = new BuyTicketThread();

        Thread t1 = new Thread(buyTicketThread);
        Thread t2 = new Thread(buyTicketThread);
        Thread t3 = new Thread(buyTicketThread);

        t1.setName("小明");
        t2.setName("小李");
        t3.setName("小张");

        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTicketThread implements Runnable{
    private int ticketNum = 10;

    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //加锁
                lock.lock();
                if (ticketNum > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "购买了第" + ticketNum-- + "张票");
                }
            }finally {
                //解锁
                lock.unlock();
            }
        }
    }
}

lock 锁住的是 Lock 对象,当调用它的 lock 方法时,会将 Lock 类中的一个标志位 state 加 1(state 其实是 AbstractQueuedSynchronizer 这个类中的一个变量,它是 Lock 中一个内部类的父类),释放锁时是将 state 减 1(加 1 减 1 这样的操作是为了实现可重入)

三、Synchronized与Lock的区别

1、Lock是一个接口;synchronized是Java内置的关键字

2、Lock可以判断是否获取到了锁;synchronized无法判断获取锁的状态

3、Lock是显式锁,必须手动关闭锁(手动开启和关闭锁,忘记关闭锁会导致 死锁) ;synchronized是隐式锁,会自动释放锁,出了作用域自动释放

4、Lock只有代码块锁,适合大量同步代码块;synchronized有代码块锁和方法锁,适合少量代码

5、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,不一定会导致线程排队等待阻塞。并且具有更好的扩展性(提供更多的子类);synchronized会导致线程排队等待阻塞

6、Lock,可重入锁,可以判断锁状态,非公平;synchronized 可重入锁,不可中断,非公平

使用优先顺序

Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)

四、Java中三大变量线程安全问题

1、Java中三大变量

1、实例变量:在堆中

2、静态变量:在方法区

3、局部变量:在栈中

局部变量永远都不会存在线程安全问题;因为局部变量不共享(一个线程一个栈)。局部变量在栈中。所以局部变量永远都不会共享

 实例变量在堆中,堆只有1个

 静态变量在方法区中,方法区只有1个

堆和方法区都是多线程共享的,所以可能存在线程安全问题

局部变量+常量:不会有线程安全问题

成员变量:可能会有线程安全问题

2、使用局部变量

建议使用:StringBuilder(线程不安全)

因为局部变量不存在线程安全问题

选择StringBuilder;StringBuffer(线程安全,源码中方法使用 synchronized修饰)效率比较低

ArrayList是非线程安全的

Vector是线程安全的

HashMap HashSet是非线程安全的

Hashtable是线程安全的。

3、开发中解决线程安全问题

我们在开发中不是一上来就选择线程同步synchronized ;synchronized会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制

第一种方案:尽量使用局部变量代替“实例变量和静态变量”

第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)

第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized(线程同步机制)

五、死锁

线程 1 持有资源 B,线程 2 持有资源 A,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态

Java--多线程之synchronized和lock;死锁(四)_第2张图片

死锁现象不会出现报错,也不会出现异常,程序一直僵持在那里,很难发现调试

1、产生死锁的必要条件

产生死锁必须具备以下四个条件:

1、互斥条件:该资源任意一个时刻只由一个线程占用

2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放

3、不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源

4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

2、避免线程死锁

上面说了产生死锁的四个必要条件,为了避免死锁,只需要破坏产生死锁的四个条件中的其中一个就可以了

1、破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)

2、破坏请求与保持条件 :一次性申请所有的资源

3、破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源

4、破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件

//死锁:
public class DeadLock {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();

        MyThread1 t1 = new MyThread1(obj1,obj2);
        MyThread2 t2 = new MyThread2(obj1,obj2);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread{
    private Object obj1;
    private Object obj2;

    public MyThread1(){
    }

    public MyThread1(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj1){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (obj2){

            }
        }
    }
}

class MyThread2 extends Thread{
    private Object obj1;
    private Object obj2;

    public MyThread2(){
    }

    public MyThread2(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj2){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (obj1){

            }
        }
    }
}

参考链接

https://www.jb51.net/article/244365.htm

你可能感兴趣的:(JavaSE,多线程)