并发编程(三)

1.Synchronized 的介绍和使用

介绍:在 Java 中,synchronized 是一种关键字,用于实现多线程之间的同步。它可以应用于方法或代码块,以确保在同一时刻只有一个线程可以访问被 synchronized 修饰的方法或代码块,从而避免多线程并发访问共享资源时产生的数据竞争和不一致性。

synchronized 的主要作用是解决多线程之间的资源竞争问题,特别是在多个线程同时修改共享数据时,可能导致数据不一致的情况。通过使用 synchronized,可以确保同一时刻只有一个线程访问共享资源,从而避免竞争条件和线程安全问题。

使用:

synchronize 的使用方式可分为应用在代码块上和运用在方法上,代码如下

public synchronized void synchronizedMethod() {
    // 同步代码块
}
public void someMethod() {
    // 非同步代码块

    synchronized (sharedObject) {
        // 同步代码块
    }

    // 非同步代码块
}

以上两种方式锁的都是对象锁(简单理解第一种,就是需要创建对象才能使用该方法,所以锁的也是对象,第二种sharedObject就是一个对象,显而易见),除了对象锁,当synchronized加在了静态方法上面时,那它就是一个类锁,锁的就是xx.class的Class对象,相当于锁的是类源信息,每个类只有一个class对象,所以每个类只有一个类锁

public class Test1 {
    private long count = 0;
    private Object object = new Object();
    public long getCount() {
        return count;
    }
    public void setCount(long count) {
        this.count = count;
    }
    
    /*不加锁则计算结果会小于等于20000*/
    public void incCountNoSyn() {
        count++;
    }
    public synchronized void incCountSyn() {
        count++;
    }
    public void incCountSynn() {
        synchronized (this) {
            count++;
        }
    }
    /*用在同步块上,但是锁的是单独的对象实例*/
    public void incCountObj() {
        synchronized (object) {
            count++;
        }
    }
    private static class Count extends Thread {
        private Test1 simplOper;
        public Count(Test1 simplOper) {
            this.simplOper = simplOper;
        }
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                simplOper.incCountObj();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Test1 test1 = new Test1();
        Count count = new Count(test1);
        Count count1 = new Count(test1);
        count.start();
        count1.start();
        Thread.sleep(50);
        System.out.println(test1.count);//20000

    }

}

上面是一段使用多线程进行累加的代码,当不加锁的时候最后的累加结果将会小于等于预期结果,可见锁的重要性,需要注意的是,锁的对象必须要是同一个才会生效,不然不会生效的。

2.volatile,最轻量的同步/通信机制

在 Java 中,volatile 是一种关键字,用于声明一个变量为“易变”的。当一个变量被声明为 volatile,它会具有一些特殊的属性,主要涉及多线程之间的可见性和禁止指令重排序。

volatile 主要用于确保多线程环境下的共享变量能够正确地被访问和修改,以避免线程之间的数据不一致问题。以下是volatile的一些特性:

  1. 可见性:volatile 变量在一个线程中被修改后,会立即被写回主内存,并通知其他线程。其他线程读取这个 volatile 变量时,会从主内存中读取最新的值,确保所有线程都能看到最新的修改。

  2. 禁止指令重排序:volatile 变量的读写操作会建立一个内存屏障,这会禁止指令重排序。这确保了在 volatile 变量之前的指令不会被重排序到 volatile 变量之后,从而保证了变量的写操作先于读操作。

public class VolatileExample {
    private volatile boolean flag = false;

    public void toggleFlag() {
        flag = !flag;
    }

    public boolean isFlag() {
        return flag;
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        Thread writerThread = new Thread(() -> {
            example.toggleFlag();
            System.out.println("Flag set to true");
        });

        Thread readerThread = new Thread(() -> {
            while (!example.isFlag()) {
                // 等待
            }
            System.out.println("Flag is true");
        });

        writerThread.start();
        readerThread.start();
    }
}

上诉这个例子就是通过volatile 保证了flag的可见性,当一个线程改变flag的值后,另一个线程可以立马察觉,并执行它的业务逻辑代码,如果不用volatile,另一个线程则无法实时获取flag的值,会有并发问题!

3.等待/通知机制
等待通知范式:

等待(Wait):一个线程调用某个对象的 wait() 方法,使自己进入等待状态,释放对该对象的锁。线程会一直等待,直到其他线程调用相同对象的 notify()notifyAll() 方法。

通知(Notify):一个线程调用某个对象的 notify()notifyAll() 方法,唤醒一个或多个等待在该对象上的线程。被唤醒的线程会重新尝试获取该对象的锁,并继续执行。

 

等待与通知范式的基本使用步骤如下:
  1. 获得对象锁:在等待线程和通知线程之间需要共享同一个对象,确保它们在操作时都获得了对象的锁。

  2. 等待条件:在等待线程中,使用 wait() 方法进入等待状态,等待某个条件的满足。

  3. 通知条件:在通知线程中,使用 notify()notifyAll() 方法唤醒等待的线程。notify() 唤醒一个等待线程,notifyAll() 唤醒所有等待线程。

  4. 释放对象锁:被唤醒的线程重新尝试获取对象的锁,从而继续执行。

import java.util.LinkedList;
import java.util.Queue;

/**
 * @Author coder huang
 * @Date 2023 08 08 15 56
 **/
public class WaitNotifyExample {
    public static void main(String[] args) {
        final Object lock = new Object();
        final int CAPACITY = 5;
        Queue<Integer> queue = new LinkedList<>();

        Thread producer = new Thread(() -> {
            synchronized (lock) {
                while (queue.size() >= CAPACITY) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                queue.offer(888);
                System.out.println("Produced 1, Queue size: " + queue.size());
                lock.notifyAll();
            }
        });

        Thread consumer = new Thread(() -> {
            synchronized (lock) {
                while (queue.isEmpty()) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int value = queue.poll();
                System.out.println("Consumed " + value + ", Queue size: " + queue.size());
                lock.notifyAll();
            }
        });

        producer.start();
        consumer.start();
    }
}

这段代码就模拟了生产者和消费者 producer 线程负责生产数据,consumer 线程负责消费数据。通过 wait()notifyAll() 方法,实现了生产者和消费者之间的协作,确保队列满时生产者等待,队列空时消费者等待。

注意:尽可能使用 notifyAll(),谨慎使用 notify(),因为 notify() 只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

面试题:调用 yield() sleep()wait()notify()等方法对锁有何影响?

  • yield()sleep() 被调用后,都不会释放当前线程所持有的锁。

  • 调用 wait() 方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。

  • 调用 notify() 系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才会自然而然地释放锁,所以 notify() 系列方法一般都是 syn 同步代码的最后一行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值