线程同步(超详细)(一)

1. 什么是线程同步?

首先,引用一个非常经典的例子来说明为什么要进行线程同步

当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。
举个例子,动物园有三个窗口同时在售卖门票,假设还剩最后一张门票时,有两个窗口同时有人在买门票,此时两个窗口都观察到还有一张门票,于是两个窗口都选择了卖出,此时门票数变成了-1,出现错误。

还可能会出现其他情况的错误,比如剩余10张票时,两个窗口同时售卖出一张票后修改票数为9。

package test;

import java.io.*;

public class TicketThreadTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread ticket1 = new TicketThread();
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket1, "窗口2");
        Thread thread3 = new Thread(ticket1, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread.sellNum);
    }
}

class TicketThread implements Runnable {

    private static int ticketNum = 20; // 总票数

    public static int sellNum = 0; // 统计卖出总票数

    @Override
    public void run() {
        while (true) {
            if (ticketNum > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
                sellNum++;  // 卖出总票数加1
            } else {
                break;
            }

            try {
                Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这是某次程序运行结果,很显然卖出29张票,发生了错误

窗口3卖出了一张票,剩余:19
窗口1卖出了一张票,剩余:18
窗口2卖出了一张票,剩余:17
窗口3卖出了一张票,剩余:16
窗口1卖出了一张票,剩余:16
窗口2卖出了一张票,剩余:16
窗口1卖出了一张票,剩余:15
窗口3卖出了一张票,剩余:14
窗口2卖出了一张票,剩余:15
窗口2卖出了一张票,剩余:13
窗口3卖出了一张票,剩余:12
窗口1卖出了一张票,剩余:13
窗口3卖出了一张票,剩余:9
窗口1卖出了一张票,剩余:11
窗口2卖出了一张票,剩余:10
窗口1卖出了一张票,剩余:7
窗口3卖出了一张票,剩余:8
窗口2卖出了一张票,剩余:8
窗口1卖出了一张票,剩余:5
窗口3卖出了一张票,剩余:6
窗口2卖出了一张票,剩余:6
窗口2卖出了一张票,剩余:4
窗口3卖出了一张票,剩余:3
窗口1卖出了一张票,剩余:4
窗口1卖出了一张票,剩余:2
窗口3卖出了一张票,剩余:2
窗口2卖出了一张票,剩余:1
窗口2卖出了一张票,剩余:-1
窗口1卖出了一张票,剩余:0
sellNum: 29

2. Java线程同步方法

Java线程同步有7种方法

  • 使用 synchronized关键字实现线程同步
  • 使用wait和notify实现线程同步
  • 使用特殊域变量(volatile)实现线程同步
  • 使用重入锁实现线程同步,在JavaSE5.0中新增了一个java.util.concurrent包来支持同步
  • 使用局部变量实现线程同步,如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
  • 使用阻塞队列实现线程同步
  • 使用原子变量实现线程同步

3 使用synchronized实现线程同步

synchronized的作用主要有三个:

  • 原子性:确保线程互斥地访问同步代码
  • 可见性:保证共享变量的修改能够及时可见

可见性是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值”来保证的

  • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”

happen-before:If one action _happens-before _another, then the first is visible to and ordered before the second. 如果指令甲happens-before指令乙,那么指令甲必须排序在指令乙之前,并且指令甲的执行结果对指令乙可见。

3.1 同步代码块

同步代码块是通过锁定一个指定的对象,来对同步代码块中的代码进行同步。 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该代码块的线程将被阻塞。
注意synchronized必须锁住的是指定的对象,不同对象间不会阻塞,如果需要锁住类对象,只需要使用synchronized(Class clazz)锁住类即可。

我们使用同步代码块来解决售票问题

package test;

import java.io.*;

public class TicketThreadTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread ticket1 = new TicketThread();
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket1, "窗口2");
        Thread thread3 = new Thread(ticket1, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread.sellNum);
    }
}

class TicketThread implements Runnable {

    private static int ticketNum = 20; // 总票数

    public static int sellNum = 0; // 统计卖出总票数

    @Override
    public void run() {
        while (true) {
            synchronized (this) {	// 锁住对共享变量的访问
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
                    sellNum++;  // 卖出总票数加1
                } else {
                    break;
                }
            }

            try {
                Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

从运行结果可以看到synchronized修饰的代码块同一时间只能有一个线程访问

窗口1卖出了一张票,剩余:19
窗口2卖出了一张票,剩余:18
窗口3卖出了一张票,剩余:17
窗口3卖出了一张票,剩余:16
窗口2卖出了一张票,剩余:15
窗口1卖出了一张票,剩余:14
窗口1卖出了一张票,剩余:13
窗口2卖出了一张票,剩余:12
窗口3卖出了一张票,剩余:11
窗口2卖出了一张票,剩余:10
窗口3卖出了一张票,剩余:9
窗口1卖出了一张票,剩余:8
窗口1卖出了一张票,剩余:7
窗口2卖出了一张票,剩余:6
窗口3卖出了一张票,剩余:5
窗口2卖出了一张票,剩余:4
窗口3卖出了一张票,剩余:3
窗口1卖出了一张票,剩余:2
窗口3卖出了一张票,剩余:1
窗口1卖出了一张票,剩余:0
sellNum: 20

注意上述类中的ticketNum和sellNum都属于类对象,如果我们使用不同的实例对象,使用synchronized(this)锁住的不是同一个对象,会发现并没有实现线程同步,此时就需要锁住synchronized(this.getClass())。

使用不同实例对象,main方法中修改如下:
TicketThread ticket1 = new TicketThread();
TicketThread ticket2 = new TicketThread();
TicketThread ticket3 = new TicketThread();
Thread thread1 = new Thread(ticket1, “窗口1”);
Thread thread2 = new Thread(ticket2, “窗口2”);
Thread thread3 = new Thread(ticket3, “窗口3”);
TicketThread类中修改为synchronized(this.getClass())或synchronized(TicketThread.class)

3.2 同步方法

同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是方法所属的对象自身。

相当于使用synchronized(this)锁住方法中的代码

如果这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,而是这个类对应的java.lang.Class类型的对象。

相当于使用synchronized(this.getClass())锁住方法中的代码

**注意:**当一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块,但可以访问非同步方法中的非同步代码块。

上述售票问题使用同步方法实现线程同步

package test;

import java.io.*;

public class TicketThreadTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread ticket1 = new TicketThread();
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket1, "窗口2");
        Thread thread3 = new Thread(ticket1, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread.sellNum);
    }
}

class TicketThread implements Runnable {

    private static int ticketNum = 20; // 总票数

    public static int sellNum = 0; // 统计卖出总票数

    @Override
    public void run() {
        while (true) {
            if(!sellOneTicket()){
                break;
            }
            try {
                Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private synchronized boolean sellOneTicket(){
        if (ticketNum > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
            sellNum++;  // 卖出总票数加1
            return true;
        } else {
            return false;
        }
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值