JAVA多线程之间实现同步+多线程并发同步解决方案

一、什么是线程安全问题 

为什么有线程安全问题?

       当多个线程同时共享同一个全局变量或静态变量,做写的操作(修改变量值)时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作时不会发生数据冲突问题。



案例:需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。
/**
 * 需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。
 * Created by yz on 2018/04/01.
 */
public class ThreadDemo {
    public static void main(String[] args) {
        // t1 t2同时共享同一变量trainCount
        ThreadTrain threadTrain = new ThreadTrain();
        Thread t1 = new Thread(threadTrain, "窗口1");
        Thread t2 = new Thread(threadTrain, "窗口2");
        t1.start();
        t2.start();
    }
}

// 售票窗口
class ThreadTrain implements Runnable{
    // 总共有100张火车票
    private int trainCount = 100;
    public void run() {
        while (trainCount > 0){
            try {
                // 休眠50秒
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 出售火车票
            sale();
        }
    }

    // 卖票方法
    public void sale(){
        System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
        trainCount--;
    }
}

运行结果:


原因解析:


卖票方法加判断,不能百分百解决问题

// 卖票方法
public void sale(){
    if(trainCount > 0){
        System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
        trainCount--;
    }
}

原因解析:


多个线程共享同一个局部变量,会发生线程安全问题吗?不会


线程安全问题,有哪些解决办法?

解决办法:
synchronized --- 自动锁
lock --- jdk1.5并发包 --- 手动锁

线程之间如何同步?
同步是保证数据原子性,原子性就是数据不能受到其他线程干扰。

二、使用同步代码块解决线程安全问题 

什么地方需要考虑加锁?
考虑在真正产生共享同一个全局变量的时候使用,不要用synchronized去包裹整个代码。

// 卖票方法
public void sale(){
    // 同步代码块 synchronized 包裹需要线程安全的问题。
    synchronized (object){
        if(trainCount > 0){
            System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
            trainCount--;
        }
    }
}
什么是同步代码块?
答:就是将可能会发生线程安全问题的代码,给包裹起来
synchronized(对象){  // 这个对象可以为任意对象  
可能会发生线程冲突的问题

}

对象如同锁,持有锁的线程可以在同步块中执行,没持有锁的线程,即使获取cup的执行权,也进不去。

使用synchronized必须有一些条件:
1.必须要有两个或者两个以上的线程需要发生同步。
2.多个线程想同步,必须使用同一把锁

3.保证只有一个线程进行执行

synchronized原理:
1.首先有一个线程已经拿到了锁,其他线程已经有cup执行权,一直排队,等待释放锁。
2.锁是在什么时候释放?代码执行完毕或者程序抛出异常都会被释放掉。
3.锁已经被释放掉的话,其他线程开始进行抢锁(资源竞争),谁抢到谁进入同步中去,其他线程继续等待。

好处:解决了多线程的安全问题
弊端:效率非常低,多个线程需要判断锁,比较消耗资源,抢锁的资源。

什么是线程之间同步?

同步是保证多个线程之间共享同一个全局变量数据安全问题,保证数据原子性。

synchronized 只能有一个线程进行执行(就像厕所只有一个坑,好多人等着上厕所,谁进入厕所谁上锁,其他人在外等待),这个线程不释放锁的话,其他线程就一直等,就会产生死锁问题。


三、同步函数使用、this锁 

同步函数
什么是同步函数?
答:在方法上加上synchronized进行修饰 称为同步函数。

// 卖票方法 
public synchronized void sale(){
    if(trainCount > 0){
        System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
        trainCount--;
    }
}
同步函数使用的是什么锁?this锁
怎么证明同步函数使用的是this锁?
两个线程之间实现同步,一个线程使用this锁同步代码块,一个线程使用同步函数,这两个线程如果同步,说明同步函数使用的是this锁。
/**
 * 需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。
 * Created by yz on 2018/04/01.
 */
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // t1 t2同时共享同一变量trainCount
        ThreadTrain threadTrain = new ThreadTrain();
        Thread t1 = new Thread(threadTrain, "窗口1");
        Thread t2 = new Thread(threadTrain, "窗口2");
        t1.start();
        Thread.sleep(40);
        threadTrain.flg = false;
        t2.start();
    }
}

// 售票窗口
class ThreadTrain implements Runnable{
    // 总共有100张火车票
    private int trainCount = 100;
    public boolean flg = true;
    public void run() {
        if(flg){
            while (trainCount > 0) {
                try {
                    // 休眠50秒
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 执行同步代码块 this锁
                sale1();
            }
        }else {
            // 执行同步函数
            while (trainCount > 0){
                try {
                    // 休眠50秒
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 执行同步函数
                sale();
            }
        }
    }

    // 卖票方法 同步函数
    public synchronized void sale(){
        if(trainCount > 0){
            System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
            trainCount--;
        }
    }

    // 卖票方法
    public void sale1(){
        // 同步代码块 synchronized 包裹需要线程安全的问题。this锁
        synchronized (this){
            if(trainCount > 0){
                System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
                trainCount--;
            }
        }
    }
}


面试问题:

一个线程使用同步函数,另一个线程使用同步代码块this锁,能够同步吗?可以同步。

一个线程使用同步函数,另一个线程使用同步代码块(非this锁),能够同步吗?不能同步。

this锁  synchronized(参数)只要是任何一个类型都可以把它叫做锁

public void sale(){
    // 同步代码块 synchronized 包裹需要线程安全的问题。
    synchronized (this){
        if(trainCount > 0){
            System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
            trainCount--;
        }
    }
}

四、静态同步代码块 

同步函数分为:

1.非静态同步函数:

public synchronized void sale(){}

2.静态(static关键字)同步函数,还会用this锁吗?
public static synchronized void sale(){}
静态同步函数不使用this锁,不同步。
当一个变量被static修饰的话存放在内存永久区,当class文件被加载的时候会被初始化。
private static int trainCount = 100;
静态同步函数与当前字节码文件一起使用可以同步。
// 卖票方法 同步函数
public static synchronized void sale(){
    if(trainCount > 0){
        System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
        trainCount--;
    }
}

// 卖票方法 
public void sale1(){
    // 同步代码块 synchronized 包裹需要线程安全的问题。this锁
    synchronized (ThreadTrain.class){
        if(trainCount > 0){
            System.out.println(Thread.currentThread().getName()+",出售第"+(100-trainCount+1)+"张票");
            trainCount--;
        }
    }
}

两个线程,一个线程使用同步函数,另一个线程使用静态同步函数能实现同步吗?

不能,同步函数使用this锁,静态同步函数使用当前字节码文件(字节码文件就是当前class文件)。

加锁保证同步,同步保证数据安全问题、原子问题。

使用synchronized、lock都属于单个jvm中同步。分布式锁、高并发和jvm同步是没有任何关系,是和集群有关系。


五、多线程死锁 

什么是多线程死锁?

答:同步中嵌套同步,导致锁无法释放



六、Java内存模型 

多线程三大特性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。
可见性:当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:程序执行的顺序按照代码先后顺序执行。

java内存模型和java内存结构区别?

java内存模型属于多线程可见性 jmm

java内存结构是jvm内存分配



七、Volatile可见性 

什么是Volatile

Volatile 关键字的作用是变量在多个线程之间可见,但不保证原子性。

线程可见性:线程1本地内存的变量值发生改变之后,立马通知给另一个线程。

/**
 * Volatile可见性
 * Created by yz on 2018/4/2.
 */
public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo td = new ThreadVolatileDemo();
        td.start();
        Thread.sleep(3000);
        // 主线程修改了共享全局变量为false
        td.setFlg(false);
        System.out.println("flg值已经修改为false!");
        Thread.sleep(1000);
        System.out.println(td.flg);

    }
}

class ThreadVolatileDemo extends Thread{

    public boolean flg = true;

    @Override
    public void run() {
        System.out.println("子线程开始执行...");
        while(flg){
      // ThreadVolatileDemo线程依然读取的是本地线程内存值 
        }
        System.out.println("子线程执行结束...");
    }

    public void setFlg(boolean flg){
        this.flg = flg;
    }
}

问题,当flg值修改为false后,程序一直停止不了


解决,使用volatile关键字强制刷新主内存

public volatile boolean flg = true;

如果没有添加休眠时间,在不添加volatile关键字时,修改flg值后,flg值是实时修改,程序执行完毕结束。
主线程添加休眠时间后,在不添加volatile关键字时,修改flg值后,不会及时通知给子线程。


八、AtomicInteger原子类


创建10个线程,每个线程执行1000次,共享count变量。

/**
 * Volatile非原子性
 * Created by yz on 2018/4/2.
 */
public class VolatileNoAtomic extends Thread{
    // 需要10个线程同时共享count  static修饰关键字,存放在静态区,只会存放一次,所有线程都会共享
    private volatile static int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            count++;
        }
        System.out.println(getName()+","+count);
    }

    public static void main(String[] args) {
        // 创建10个线程
        VolatileNoAtomic[] vaList = new VolatileNoAtomic[10];
        for (int i = 0; i < vaList.length; i++) {
            vaList[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < vaList.length; i++) {
            vaList[i].start();
        }
    }
}

加上volatile关键字数据也不准确


java并发包:java.util.concurrent  jdk1.5


AtomicInteger原子类,用于计数

/**
 * AtomicInteger原子类
 * Created by yz on 2018/4/2.
 */
public class VolatileNoAtomic extends Thread{
    // 需要10个线程同时共享count  static修饰关键字,存放在静态区,只会存放一次,所有线程都会共享
    //private volatile static int count = 0;
    private static AtomicInteger count = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            //count++;
            // 以原子方式将当前值加 1
            count.incrementAndGet();
        }
        System.out.println(getName()+","+count.get());
    }

    public static void main(String[] args) {
        // 创建10个线程
        VolatileNoAtomic[] vaList = new VolatileNoAtomic[10];
        for (int i = 0; i < vaList.length; i++) {
            vaList[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < vaList.length; i++) {
            vaList[i].start();
        }
    }
}

九、ThreadLock原理剖析 


import java.util.Map;

/**
 * Created by yz on 2018/04/02.
 */
public class ThreadLocal01 {
    public static void main(String[] args) {
        // 怎样不共享ResNumber,每个线程都用自己的ResNumber,不使用new
//        ResNumber resNumber1 = new ResNumber();
//        ResNumber resNumber2 = new ResNumber();
//        ResNumber resNumber3 = new ResNumber();
        ResNumber resNumber = new ResNumber();
        LocalThreadDemo t1 = new LocalThreadDemo(resNumber);
        LocalThreadDemo t2 = new LocalThreadDemo(resNumber);
        LocalThreadDemo t3 = new LocalThreadDemo(resNumber);
        t1.start();
        t2.start();
        t3.start();
    }
}

/**
 * 使用ThreadLocal , ResNumber不共享,在每个线程中使用,互不影响
 */
class ResNumber{
     public int count = 0;

    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0;
        }
    };

    public String getNumber(){
        count = threadLocal.get()+1;
        threadLocal.set(count);
        return count+"";
    }

    // ThreadLocal get set伪代码
    Map<Object,Object> map;
    public void set(Integer count){
        // Thread.currentThread() 获取当前线程的引用
        map.put(Thread.currentThread(),count);
    }
    public String get(){
        return (String) map.get(Thread.currentThread());
    }
}

class LocalThreadDemo extends Thread{
    private ResNumber resNumber;
    public LocalThreadDemo(ResNumber resNumber){
        this.resNumber = resNumber;
    }
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(getName()+","+resNumber.getNumber());
        }
    }
}


多线程并发同步解决方案

业务场景1:

业务需求1:假如现在有20个人去售票厅窗口买票,但是窗口只有2个,那么同时能够买票的只能有2个人,当2个人中任意1个人买完票离开窗口之后,等待的18个人中又会有一个人可以占用窗口买票。

真实需求:控制并发数为2
拆解转化业务需求:
人=线程
2个窗口=资源
在窗口买票=表示线程正在执行
离开售票窗口=线程执行完毕
等待买票=线程阻塞,不能执行

解决方案:信号量 Semaphore

应用场景:用于流量控制,限流

代码:

/**
 * 信号量 Semaphore
 * Created by yz on 2018/3/4.
 */
public class SemaphoreDemo {

    class MyTask implements Runnable{

        private Semaphore semaphore; // 信号量
        private int user;  // 第几个用户

        public MyTask(Semaphore semaphore, int user) {
            this.semaphore = semaphore;
            this.user = user;
        }

        @Override
        public void run() {
            try {
                // 获取信号量许可,才能占用窗口
                semaphore.acquire();
                // 运行到这里说明获取到了许可,可以去买票了
                System.out.println("用户"+ user + "进入窗口,准备买票...");
                Thread.sleep((long)Math.random()*10000); // 模拟买票时间
                System.out.println("用户"+ user + "买票完成,准备离开...");
                Thread.sleep((long)Math.random()*10000);
                System.out.println("用户"+ user + "离开售票窗口...");
                // 释放信号量许可证
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void execute(){
        // 定义窗口个数
        final Semaphore s = new Semaphore(2);
        // 线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();

        // 模拟20个用户
        for (int i = 0; i < 20; i++) {
            threadPool.execute(new MyTask(s,(i+1)));
        }
        // 关闭线程池
        threadPool.shutdown();
    }

    public static void main(String[] args) {
        SemaphoreDemo semaphoreDemo = new SemaphoreDemo();
        semaphoreDemo.execute();
    }

}

业务场景2:

业务需求2:公司周末组织去聚餐,首先各自从家里出发到聚餐地点,当所有人全部到齐之后,才开始吃饭 如果人员未到齐,到的人就只能等待在那里,直到所有人都到齐之后才能吃饭或者做后面的事情。
 
解决方案:同步屏障 CyclicBarrier

应用场景:用于多线程计算数据,最后合并计算结果,例如多个老师打分,最后合并算平均分

代码:

/**
 * 同步屏障 CyclicBarrier
 * Created by yz on 2018/3/4.
 */
public class CyclicBarrierDemo {

    public static void main(String[] args) {
        final CyclicBarrier cb = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                // 在吃饭之前做点别的事情
                System.out.println("人员全部到齐了,拍照留念...");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        // 模拟3个用户
        for (int i = 0; i < 3; i++) {
            final int user = i+1;
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    try {
                        // 模拟每个人来的时间各不一样
                        Thread.sleep((long)Math.random()*10000);
                        System.out.println(user+"到达聚餐地点,当前已有"+ (cb.getNumberWaiting()+1) +"人到达");
                        // 设置屏障 等待,只有当线程都到达之后,才能往下面走
                        cb.await();
                        if(user == 3){
                            System.out.println("人员全部到齐,开始吃饭...");
                        }
                        Thread.sleep((long)Math.random()*20000);
                        System.out.println(user+"吃完饭了,准备回家...");
                        // CyclicBarrier 可以重复使用 doSomething


                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            threadPool.execute(r);
        }
        threadPool.shutdown();
    }

}

业务场景3:

业务需求3:假如A团伙绑了B,告诉B的家人C,需要1000万赎人,A与C达成一致意见到某一个地点交换人质,于是,A团伙和C同时到达交换地点,然后同时一手交钱一手交人质。
解决方案:Exchanger 两个线程之间进行数据交换

应用场景:用于两个线程之间交换数据,例如校对工作

代码:

/**
 * Exchanger 两个线程交换数据
 * Created by yz on 2018/3/4.
 */
public class ExchangerDemo {
    public static void main(String[] args) {
        // 定义交换器,交换String 类型的数据,当然是可以为任意类型
        Exchanger<String> exchanger = new Exchanger<>();
        // 定义线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        // 绑架者A
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // 准备人质
                    String renzhi = "B";
                    String money = exchanger.exchange(renzhi);
                    System.out.println("绑架者用B交换回:"+money);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 家属C
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // 准备1000万
                    String money = "1000万";
                    String renzhi = exchanger.exchange(money);
                    System.out.println("C用1000万交换回:"+renzhi);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadPool.shutdown();
    }
}

业务场景4:

业务需求4:比如有一个任务A,他需要等待其他几个任务(BCD)都执行完毕之后才能执行这个任务
解决方案:ConutDownLatch 倒计时器
应用场景:可以用于模拟高并发

ConutDownLatch 与 CyclicBarrier 区别:
共同点:都能够实现线程之间的等待
不同点:
        ConutDownLatch 一般用于某个线程A等待若干个其他线程执行完任务之后,它才能执行
CyclicBarrier 一般用于一组线程互相等待到某个状态,然后这一组线程在同时执行
ConutDownLatch 是不能重用的,CyclicBarrier 可以重复使用

代码:

/**
 * ConutDownLatch 倒计时器
 * Created by yz on 2018/3/4.
 */
public class ConutDownLatchDemo {
    public static void main(String[] args) {
        // 定义倒计时器
        final CountDownLatch latch = new CountDownLatch(3);
        // 模拟一个子任务B
        new Thread(){
            @Override
            public void run() {
                try {
                    // 模拟任务执行时间
                    Thread.sleep((long)Math.random()*10000);
                    System.out.println("子任务B"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep((long)Math.random()*10000);
                    System.out.println("子任务B"+Thread.currentThread().getName()+"执行完毕");
                    // 倒计时减掉1
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        // 模拟一个子任务C
        new Thread(){
            @Override
            public void run() {
                try {
                    // 模拟任务执行时间
                    Thread.sleep((long)Math.random()*10000);
                    System.out.println("子任务C"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep((long)Math.random()*10000);
                    System.out.println("子任务C"+Thread.currentThread().getName()+"执行完毕");
                    // 倒计时减掉1
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        // 模拟一个子任务D
        new Thread(){
            @Override
            public void run() {
                try {
                    // 模拟任务执行时间
                    Thread.sleep((long)Math.random()*10000);
                    System.out.println("子任务D"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep((long)Math.random()*10000);
                    System.out.println("子任务D"+Thread.currentThread().getName()+"执行完毕");
                    // 倒计时减掉1
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        // main 线程为主任务A
        System.out.println("等待3个子任务执行完毕"+Thread.currentThread().getName()+"主任务才开始执行");
        try {
            // 等待子任务执行完毕 此时阻塞
            latch.await();
            System.out.println("说明BCD三个子任务已经执行完毕");
            // 继续执行主任务
            System.out.println("继续执行主任务:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}


发布了43 篇原创文章 · 获赞 32 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/yz2015/article/details/79436123