Java多线程详解(线程同步)

        嗨喽~小伙伴们我来了,

        上一章,我们通过几个例子(点击跳转)介绍了线程安全问题,而说到线程安全就不得不提到线程同步,它是解决线程安全的一种重要方法。本章就来简单地介绍一下线程同步

        从上一章的学习我们知道,当多个线程操作一个资源的时候,有可能由于线程的不确定切换出现数据不一致的安全问题,因此,我们要解决这个问题,就得想办法使得资源在某个时间戳只能被一个线程访问。基于这样的思想,我们提出了“队列与锁”的策略:

        通俗理解,就是将所有线程排成一个队列,给要共享的资源上把锁,只有线程获得该资源的锁之后,才能访问该资源

        这也是线程同步的基本思想。所谓线程同步,是指同一进程中的多个线程相互协调工作从而达到一致性。使用线程同步,在解决线程安全问题的同时还能提高程序的性能。

        基于“队列与锁”的思想,JDK中封装了一些用于实现线程同步的类和关键字。我们主要介绍synchronized 和 lock 两种。

一. synchronized

        我们知道,在java中,每个对象都有一把唯一的锁,这也是synchronized实现线程同步的基础。总的来说,synchronized实现线程同步主要有三种形式:

形式特点
实例同步方法锁的是当前实例对象,执行同步代码前必须获得当前实例的锁
静态同步方法锁的是当前类的class对象,执行同步代码前必须获得当前类对象的锁
同步代码块锁的是括号里的对象,对给定对象加锁,执行同步代码块必须获得给定对象的锁

        事实上,当两个线程同时对一个对象的某个方法进行调用时,只有一个线程能够抢到该对象的锁,因为一个对象只有一把锁。抢到该对象的锁之后,其他线程就无法访问该对象的所有synchronized方法,但仍可以访问该对象中的非synchronized方法

        下面,我们用代码来演示synchronized的三种用法。为了突出synchronized在线程安全方面的作用,我们采用对比(有synchronized和无synchronized)的方式来介绍。

        第一种,修饰实例同步方法。首先,来看一个简单的程序:



/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    //公共资源
    static int count = 0;
    public void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Synchronized(),"A").start();
        new Thread(new Synchronized(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

        如果小伙伴们学习了上一章的内容,应该很容易看出这个程序是存在线程安全问题的,它的输出结果如下:

        如果小伙伴们不太理解的话,可以去看我的上一篇博文。 

        上一篇博文中说到,对于“count++”,JVM是这样处理的:

1. 某线程从内存中读取count值到寄存器

2. 某线程在寄存器中修改count的值

3. 某线程将修改后的count值写入内存

        简单解释一下,我们开启了两个线程去执行increase()方法,如果没有任何保护机制,假设,当count的值累加到1000时,A线程从主内存中读取到寄存器的count值为1000,执行完“count++”操作后,寄存器中的count值为1001,刚想写入内存,B线程正好抢到了CPU的使用权,开始执行run()方法,由于未写入内存,B线程从内存中读取到的count值为仍为1000,执行完“count++”操作后,B线程成功地将1001写入内存,接着A线程将自己寄存器中的count值1001写入内存(覆盖了B线程的1001),由此导致,虽然执行了两个线程,但count的值只累加了一次,这样的情况多发生几次,计算结果自然就低于20000了。

        为了避免以上情况发生,我们给increase()方法加上修饰符synchronized,使得两个线程无法同时调用increase()方法,以保证上面的三步中的任何一步都不会被另外一个线程打断。

         这样,“count++”操作就永远不会因线程切换而出错。代码如下:



/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    //公共资源
    static int count = 0;
    //synchronized实现线程同步
    public synchronized void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Synchronized(),"A").start();
        new Thread(new Synchronized(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

        现在来看运行结果:

        没有任何问题。

        此外,使用synchronized时,有几个需要注意的地方,请看下面的代码:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public synchronized void running() throws InterruptedException {
        System.out.println("1");
        Thread.sleep(1000);
        System.out.println("2");
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //用同一个类创建两个对象
        Synchronized sync1 = new Synchronized();
        new Thread(sync1).start();
        Synchronized sync2 = new Synchronized();
        new Thread(sync2).start();
    }
}

        如果我们使用不同的对象访问,那么结果可能是不同步的

        因为synchronized修饰实例方法时锁的对象是this对象,而使用两个对象去访问,不是同一把锁。如果我们用同一对象访问:

//只创建一个对象
Synchronized sync = new Synchronized();
new Thread(sync).start();
new Thread(sync).start();

        那结果是同步的:

          接着,我们来看synchronized修饰静态方法的例子:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public static synchronized void running() throws InterruptedException {
        System.out.println("1");
        Thread.sleep(1000);
        System.out.println("2");
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //用同一个类创建两个对象
        Synchronized sync1 = new Synchronized();
        new Thread(sync1).start();
        Thread.sleep(2000);
        Synchronized sync2 = new Synchronized();
        new Thread(sync2).start();
    }
}

        虽然有sync1,sync2两个对象,但是它们是同一个类对象(Synchronized.class)产生的,而synchronized修饰静态方法时锁的是 Synchronized.class,因此两个线程仍然是同步的:

         最后一种,synchronzied修饰同步代码块,它可以锁任何指定的对象,语法示例如下:

synchronized (this){
      System.out.println("1");
      Thread.sleep(1000);
      System.out.println("2");
}

        this指代当前实例对象,可以换为任何对象。

        那么为什么要使用同步代码块呢?

        在某些情况下,我们编写的方法体可能比较庞大,同时又有一些耗时的操作,如果对整个方法体进行同步,效率会大大降低,所以我们希望能够只同步必要的代码块,对于一些不需要同步的或者耗时较长的操作,放到同步代码块之外,比如:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public void running() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            System.out.println("这是耗时操作。");
        }
        //需要同步的代码块写下面
        synchronized (this){
            System.out.println("1");
            Thread.sleep(1000);
            System.out.println("2");
        }
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        new Thread(sync).start();
        new Thread(sync).start();
    }
}

        运行结果如下:

        再运行一次:

        结果表明,需要同步的代码块确实实现了同步。 

二.  lock         

        前面使用的synchronized关键字可以实现多线程间的同步互斥,其实,在JDK1.5后新增的ReentrantLock 类同样可以实现这个功能,而且在使用上比 synchronized ​​​​​​​更为灵活。

        翻阅源码,可以看到 ReentrantLock 类实现了Lock接口:

         下面我们用ReentrantLock类来实现简单的线程同步:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote ReentrantLock实现线程同步
 */
public class LockDemo implements Runnable{
    private Lock lock = new ReentrantLock();
    /**
     * 
     * 按照规范,
     * lock()后面应紧跟try代码块,
     * 并将unlock()放到finally块的第一行
     * 
     */
    @Override
    public void run() {
        //上锁
        lock.lock();
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "线程中的i=" + i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
    public static void main(String[] args) 
        new Thread(new LockDemo(),"A").start();
        new Thread(new LockDemo(),"B").start();
    }
}

        其实关于Lock 和 ReentrantLock,还有许多其他的用法,限于篇幅,就不一一介绍了,有兴趣的小伙伴们可以查阅相关资料。

        了解了synchronized和ReentrantLock,对于上一章提出的三个线程安全问题,便可以轻松地解决了。下面提供使用synchronized的解决方式:


/**
 * @author sixibiheye
 * @date 2021/8/28
 * @apiNote 线程安全问题一-------取钱问题
 */
public class UnsafeBank {

    public static void main(String[] args) {
        //账户
        Account account = new Account(100,"买房基金");
        //你和你的妻子都要取钱
        Drawing you = new Drawing(account,50,"你");
        Drawing girlFriend = new Drawing(account,100,"妻子");
        you.start();
        girlFriend.start();
    }
}

//账户
class Account{
    int money; //余额
    String name; //卡名
    public Account(int money,String name){
        this.money = money;
        this.name = name;
    }
}

//银行
class Drawing extends Thread{
    Account account; //账户
    int drawingMoney; //取的钱
    int nowMoney; //现手上有的钱
    public Drawing(Account account,int drawingMoney,String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }
    @Override
    public void run() {
        //注意锁的是account对象,而不是this
        synchronized (account){
            if(account.money - drawingMoney < 0){
                System.out.println(Thread.currentThread().getName() + "钱不够了,取不了!");
                return;
            }
            try {
                sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卡内余额
            account.money = account.money - drawingMoney;
            //手里的钱
            nowMoney = nowMoney + drawingMoney;
            //打印
            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(Thread.currentThread().getName() + "手里的钱:" + nowMoney);
        }
        }
}


/**
 * @author sixibiheye
 * @date 2021/8/27
 * @apiNote 线程安全问题一-------买票问题
 */
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket,"小红").start();
        new Thread(buyTicket,"小白").start();
        new Thread(buyTicket,"小黑").start();
    }
}

class BuyTicket implements Runnable{
    //票数
    private int tickets = 10;
    //线程停止的标志位
    private boolean flag = true;
    //直接同步实例方法即可
    private synchronized void buy() throws InterruptedException {
        //判断是否有票
        if(tickets <= 0){
            flag = false;
            return;
        }
        //模拟延时,保证多人都能买到票
        Thread.sleep(20);
        //买票
        System.out.println(Thread.currentThread().getName() + "拿到了第" + tickets-- +"张票");

    }
    @Override
    public void run() {
        while (flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


import java.util.ArrayList;
import java.util.List;

/**
 * @author sixibiheye
 * @date 2021/8/28
 * @apiNote 线程安全问题一-------列表问题
 */

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread( () -> {
                //同步需要修改的list对象
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        //sleep保证上述for循环跑完再输出
        Thread.sleep(3000);
        //输出列表大小
        System.out.println(list.size());
    }
}

        小伙伴们可以思考一下如果使用ReentrantLock应该如何解决。

        下一章,我们将着重介绍Thread类中的一些常用方法(点击跳转),好啦~本章的内容到这就结束了,喜欢的小伙伴们点个赞鼓励支持一下吧~ 

         


版权声明:本文为sixibiheye原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。