【Java】深入理解Java中的多线程同步机制

一、多线程的数据不一致

       当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
       这个时候,一个在单线程模型下不存在的问题就会发生:如果多个线程同时读写共享变量,会出现数据不一致的问题,所以必须保证是原子操作。原子操作是指不能被中断的一个或一系列作

       通过加锁解锁的操作,即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical section),任何时候临界区最多只有一个线程能执行

二、synchronized关键字

       保证一段代码的原子性就是通过加锁和解锁实现的。在Java 的多线程模型中使用 synchronized 关键字对一个对象进行加锁。

        解决多线程并发执行时的线程同步问题(不安全、不同步),例如:解决多线程递增&&递减:

        方式一:使用synchronized代码块

public class Main {
    public static void main(String[] args) throws InterruptedException {

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new AddThread();
        Thread dec = new DecThread();

        add.start();
        dec.start();

        add.join();
        dec.join();
        System.out.println(Counter1.count);
    }
}

class Counter1 extends Thread {
    public static int count = 0;

    // 创建Object对象,用于实现同步锁
    public final static Object LOCK = new Object();
}

class AddThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (Counter1.LOCK) {  // 加锁
                Counter.count += 1;
            } // 释放锁
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (Counter1.LOCK) {  // 加锁
                Counter1.count -= 1;
            }
        } // 释放锁
    }
}

        方式二:使用this对象作为锁

public class Main {
    public static void main(String[] args) throws InterruptedException {

        Counter2 cou = new Counter2();

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new Thread(()->{
            cou.add();
        });

        Thread dec = new Thread(()->{
            cou.dec();
        });

        add.start();
        dec.start();

        add.join();
        dec.join();

        System.out.println(Counter2.count);
    }
}

class Counter2{
    public static int count=0;

    // 递增
    // 在方法声明上使用synchronized关键字,对整个方法体进行加锁,使用this对象作为锁
    public synchronized void add(){
        for (int i=0;i<100;i++){
            Counter2.count+=1;
        }
    }

    // 递减
    // 作用同步
    public void dec(){
        synchronized (this){
            for (int i=0;i<100;i++){
                Counter2.count-=1;
            }
        }
    }
}

        方式三:使用Class对象作为锁

public class Main {
    public static void main(String[] args) throws InterruptedException {

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new Thread(() -> {
            Counter3.add();
        });

        Thread dec = new Thread(() -> {
            Counter3.dec();
        });

        add.start();
        dec.start();

        add.join();
        dec.join();

        System.out.println(Counter3.count);
    }
}

class Counter3 {
    public static int count = 0;

    // 在静态方法声明上使用synchronized关键字,对整个方法体进行加锁
    // 使用Class对象作为锁
    public synchronized static void add() {
        for (int i = 0; i < 100; i++) {
            Counter3.count += 1;
        }
    }

    // 作用等同
    public static void dec() {
        synchronized (Counter3.class) {
            for (int i = 0; i < 100; i++) {
                Counter3.count -= 1;
            }
        }

    }
}

        方式四:使用具备原子性操作的参数类型

public class Main {
    public static void main(String[] args) throws InterruptedException {

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new Thread(() -> {
            Counter4.add();
        });

        Thread dec = new Thread(() -> {
            Counter4.dec();
        });

        add.start();
        dec.start();

        add.join();
        dec.join();

        System.out.println(Counter4.count);
    }
}

class Counter4 {
    // 具备原子性操作的AtomicInteger
    public static AtomicInteger count = new AtomicInteger(0);

    public static void add() {
        for (int i = 0; i < 100; i++) {
            Counter4.count.incrementAndGet();  // 自增+1
        }
    }

    public static void dec() {
        for (int i = 0; i < 100; i++) {
            Counter4.count.decrementAndGet();  // 自减-1
        }
    }
}

        使用 synchronized 解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为 synchronized 代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized 会降低程序的执行效率。

       概括总结一下如何使用 synchronized:

  •        找出修改共享变量的线程代码块
  •        选择一个共享实例作为锁
  •        使用 synchronized(lockobject){ ... }

三、注意事项

1.抛出异常

        在使用 synchronized 的时候,不必担心抛出异常。因为无论是否有异常,都会在 synchronized 结束处正确释放锁。

2.不同的lock

       使用 synchronized 的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。

3.不需要synchronized的操作

       JVM 规范定义了几种原子操作:

  • 基本类型( long 和 double 除外)赋值,例如:intn=m;

        long和 double是64 位数据,JVM 没有明确规定 64 位赋值操作是不是一个原子操作,不过在 x64 平台的 JVM 是把 long 和 double 的赋值作为原子操作实现的。

  • 引用类型赋值,例如:List<string>list=anotherList。

       注意:单条原子操作的语句不需要同步,但是,如果是多行赋值语句,就必须保证是同步操作。

四、案例

1.多线程售票

public class TicketPool  implements Runnable{

    // 当前剩余门票数
    private int ticketNum;

    // 创建公共票池,传入默认总门票数
    public TicketPool(int ticketNum){
        this.ticketNum=ticketNum;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"准备开始卖票");

        // 线程竞争CPU执行权(this锁)
        synchronized(Thread.currentThread()){
            while (true){
                if (ticketNum<=0){
                    System.out.println(Thread.currentThread().getName()+"卖完了");
                    return;
                } else{
                    System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--ticketNum)+"张票");
                }

                try {
                    // 休眠过程中:当前线程不会让出持有的"this锁",此处为引发错误的原因
                    // Thread.sleep(1000);  // 不会释放锁

                    // 注意:等待过程中,当前线程让出持有的"this锁",允许其它线程参与竞争CPU执行权(this锁)
                    this.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class SaleTicket {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个公共票池
        TicketPool ticketPool = new TicketPool(50);

        // 模拟三个售票窗口
        Thread t1 = new Thread(ticketPool);
        Thread t2 = new Thread(ticketPool);
        Thread t3 = new Thread(ticketPool);

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

 2.多线程打印数字+字母

        Character类打印字母:

public class Character  implements Runnable{
    private Object lock;
    public Character(Object lock){
        this.lock=lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            for (char i='A';i<='Z';i++){
                // 输出当前字母
                System.out.print(i);

                // "唤醒" 数字线程
                lock.notify();  // notifyAll  唤醒所有线程

                if (i<'Z'){
                    try {
                        // 字母线程,每打印一个字母,进入等待状态
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }
}

        Number类打印数字:

public class Number implements Runnable {
    private Object lock;

    public Number(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 1; i < 53; i++) {
                // 有两个数字+字母之间输出1个空格,用于分隔
                if (i % 2 == 1) {
                    System.out.print(" ");
                }
                // 输出当前数字
                System.out.print(i);

                // 当前数字是偶数,需要进入等待状态
                if (i % 2 == 0) {
                    // "唤醒" 字母线程
                    lock.notify();   // notifyAll 唤醒所有线程
                    try {
                        lock.wait();  //当前线程进入等待状态,并释放锁,( 被唤醒后,继续从等待的地方接着执行)
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }

    }
}

        测试类:

public class Test {
    public static void main(String[] args) {
        // 公共锁对象
        final Object LOCK = new Object();

        // 创建两个线程,分别执行Number和Character,共用“一把锁”
        Thread t1 = new Thread(new Number(LOCK));
        Thread t2 =new Thread(new Character(LOCK));

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

五、总结

  • 多线程同时读写共享变量时,可能会造成逻辑错误,因此需要通过synchronized 同步;
  • 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
  • 注意加锁对象必须是同一个实例;
  • 对 JVM 定义的单个原子操作不需要同步。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值