Java=线程安全解决synchronized ; Lock;并发包CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentHashMap等

使用Lock锁解决线程安全问题牌【重点】
    Lock lock = new ReentrantLock();
    lock.lock();
         需要同步的代码(需要保证原子性的代码)
    lock.unlock();

volatile关键字和synchronized关键字的区别
    volatile 能解决有序性和可见性
    原子类 能解决变量操作的原子性(有序性和可见性)
    synchronized 能解决多句代码的原子性(有序性和可见性)

CopyOnWriteArrayList类的作用
    代替多线程的情况,线程安全的ArrayList集合
CopyOnWriteArraySet类的作用
    代替多线程的情况,线程安全的HashSet集合
ConcurrentHashMap类的作用
    代替多线程的情况,线程安全的HashMap集合(比HashTable效率更好)
CountDownLatch类的作用
    可以允许一个线程等待另外一个线程执行完毕后再继续执行
CyclicBarrier类的作用
    让一组线程都到达某种条件后再执行某个任务
Semaphore类的作用
    控制多线程并发的最大数量
Exchanger类的作用
    用于线程间的通信(数据交换)

 

 

一。synchronized关键字

1.多行代码的原子性问题:

AtomicInteger类只能保证 变量 的原子性操作,而对多行代码进行原子性操作,使用AtomicInteger类就达不到效果了。

即 无法解决 多行代码的原子性。

多行代码的原子性安全问题:卖票案例:

卖票案例:
/**
 * 卖票任务
 */
public class TicketTask implements Runnable{
    //定义变量,表示初始有100张票
    int count = 100;

    @Override
    public void run() {
        while (true){
            if (count > 0) {
                //先判断,后卖票
                try {Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("卖出第"+count+"张票!");
                //票数要减少
                count--;
            }else {
                break;
            }
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        //0.创建卖票任务
        TicketTask tt = new TicketTask();
        //1.创建卖票窗口
        Thread t1 = new Thread(tt);
        Thread t2 = new Thread(tt);
        Thread t3 = new Thread(tt);
        //2.开始卖票
        t1.start();
        t2.start();
        t3.start();
        //线程出现安全问题:
        //a.可能出现重复数据
        //b.可能出现0,甚至-1这种非法数据
    }
}
出现了线程安全问题(原子性问题):
	//a.可能出现重复数据
		原因: 当一个线程执行完卖票后,还没有来得及对票数-1,被其他线程抢走了CPU,导致其他线程也卖出一样的票
    //b.可能出现0,甚至-1这种非法数据
        原因: 当剩下最后一张票时,多线程都经过了if判断 进入卖票的代码块中,然后依次卖出第1张,第0张,第-1  

synchronized 关键字介绍

synchronized 是java提供的关键字

作用:可以让多句代码具有原子性。(当某个线程执行多句代码的过程中不被其他线程打断)

解决方案一:

同步代码块

格式:
	synchronized(锁对象){
        需要同步的代码(需要保持原子性的代码)
    }

解决代码:
	/**
 * 卖票任务
 */
public class TicketTask implements Runnable{
    //定义变量,表示初始有100张票
    int count = 100;
    Object obj = new Object();
    @Override
    public void run() {
        while (true){
            //同步代码块
            synchronized (obj){
                if (count > 0) {
                    //先判断,后卖票
                    System.out.println("卖出第"+count+"张票!");
                    //票数要减少
                    count--;
                }
            }

        }
    }
}

解决方案二:

同步方法:

格式:
	public synchronized void 方法名(){
     	  需要同步的代码(需要保证原子性的代码)
    }

解决代码:
	/**
 * 卖票任务
 */
public class TicketTask implements Runnable{
    //定义变量,表示初始有100张票
    int count = 100;

    @Override
    public void run() {
        while (true){
           sellTicket();
        }
    }

    public synchronized void sellTicket(){
        if (count > 0) {
            //先判断,后卖票
            System.out.println("卖出第"+count+"张票!");
            //票数要减少
            count--;
        }
    }  
}
注意:
	a.同步代码块和同步方法其实原理是差不多,同步代码块的同步锁我们需要自己指定,而同步方法的同步锁
    ,默认使用当前对象this
    b.同步方法能否是静态方法呢? 可以是static的,如果同步方法是static的,
	默认使用当前类的字节码文件作为锁对象 

解决方案三

Lock锁

java.util.concurrent.locks.Lock 机制提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操
, 同步代码块 / 同步方法具有的功能 Lock 都有 , 除此之外更强大
Lock 锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock() : 加同步锁。
public void unlock() : 释放同步锁
Lock是一个接口,我们需要使用其实现类 ReentrantLock
ReentrantLock的API:
	public ReentrantLock();  
	public void lock(); //加锁
	public void unlock();//解锁(释放锁)
格式:
	ReentrantLock lock = new ReentrantLock();
	lock.lock(); //加锁
		需要同步的代码块(需要保持原子性的代码)
    lock.unlock();//解锁        

解决代码:
/**
 * 卖票任务
 */
public class TicketTask implements Runnable{
    //定义变量,表示初始有100张票
    int count = 100;
    //创建一个Lock锁
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            //加锁
            lock.lock();
                if (count > 0) {
                    //先判断,后卖票
                    System.out.println("卖出第"+count+"张票!");
                    //票数要减少
                    count--;
                }
            lock.unlock();
        }
    }
}

二,并发包

这是JDK提供的,包含一个在高并发情况使用集合或者工具,使用这些集合或者工具类时,能保证高并发情况下是安全的

1.CopyOnWriteArrayList

ArrayList 是线程不安全的

public class MyThread extends Thread {

    public static List<Integer> list = new ArrayList<>();//线程不安全的

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
        System.out.println("添加完毕!");
    }
}

public class TestArrayList {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("最终集合的长度:" + MyThread.list.size());
    }
}

CopyOnWriteArrayList是线程安全的

public class MyThread extends Thread {

//    public static List<Integer> list = new ArrayList<>();//线程不安全的
    public static List<Integer> list = new CopyOnWriteArrayList<>();//保证线程安全

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
        System.out.println("添加完毕!");
    }
}

2.CopyOnWriteArraySet

HashSet是线程不安全的

public class MyThread extends Thread {

    public static Set<Integer> set = new HashSet<>();//线程不安全的

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            set.add(i);
        }
        System.out.println("添加完毕!");
    }
}

public class TestSet {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.start();
        //主线程也添加10000个
        for (int i = 10000; i < 20000; i++) {
            MyThread.set.add(i);
        }

        Thread.sleep(1000 * 3);
        System.out.println("最终集合的长度:" + MyThread.set.size());
    }
}

CopyOnWriteArraySet是线程安全的

public class MyThread extends Thread {

    //public static Set<Integer> set = new HashSet<>();//线程不安全的
	public static Set<Integer> set = new CopyOnWriteArraySet<>();//线程安全的

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            set.add(i);
        }
        System.out.println("添加完毕!");
    }
}

3.ConcurrentHashMap

HashMap是线程不安全的

public class MyThread extends Thread {

    public static Map<Integer, Integer> map = new HashMap<>();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            map.put(i, i);//0,0 1,1 2,2 3,3 ... 9999,9999
        }
    }
}

public class TestMap {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.start();

        for (int i = 10000; i < 20000; i++) {
            MyThread.map.put(i, i);
        }

        Thread.sleep(1000 * 2);
        System.out.println("map最终大小:" + MyThread.map.size());
    }
}

Hashtable是线程安全的,但效率低

HashTable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下 HashTable 的效率非常低下。因为
当一个线程访问 HashTable 的同步方法,其他线程也访问 HashTable 的同步方法时,会进入阻塞状态。如线程 1 使用
put 进行元素添加,线程 2 不但不能使用 put 方法添加元素,也不能使用 get 方法来获取元素,所以竞争越激烈效率越
低。
public class MyThread1 extends Thread {
    public static Map<Integer, Integer> map = new Hashtable<>(); //线程是安全

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            map.put(i, i);
        }
        long end = System.currentTimeMillis();
        System.out.println((end - start) + " 毫秒");
    }
}

public class TestMap1 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new MyThread1().start();//开启1000个线程
        }
        Thread.sleep(1000 * 20);//由于每个线程执行时间稍长,所以这里多停顿一会
        System.out.println("map的最终大小:" + MyThread1.map.size());
    }
}

ConcurrentHashMap既安全又效率高

public class MyThread1 extends Thread {
//    public static Map<Integer, Integer> map = new Hashtable<>(); //线程是安全,但是效率低
    public static Map<Integer, Integer> map = new ConcurrentHashMap<>(); //线程是安全,但是效率高

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            map.put(i, i);
        }
        long end = System.currentTimeMillis();
        System.out.println((end - start) + " 毫秒");
    }
}

小结:
	HashMap 线程不安全(多线程不能操作同一个HashMap)
    HashTable 线程安全的(多线程可以操作同一个HashTable),但是效率较低
    ConcurrentHashMap 线程安全的,并且效率比较高   

为什么Hashtable效率低而ConcurrentHashMap效率高?

因为HashTable对哈希表进行全表加锁
而ConcurrentHashMap只对某个桶(链表)局部加锁,并且也同时使用CAS机制 

4.CountDownLatch

允许当前线程,等待其他线程完成某种操作之后,当前线程继续执行

构造方法:
    public CountDownLatch(int count); 需要传入计数器,需要等待的线程数
成员方法:
    public void await() throws InterruptedException// 让当前线程等待
    public void countDown() // 计数器进行减1   

需求: 
	线程1要执行打印:A和C,线程2要执行打印:B
    我们需要这样的结果: 线程1 先打印A 线程2打印B 之后 线程1再打印C    
        			A  B  C

public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        //0.创建一个CountDownLatch
        CountDownLatch latch = new CountDownLatch(1);
        //1.创建两个线程
        Thread t1  = new MyThread1(latch);

        Thread t2 = new MyThread2(latch);

        t1.start();

        Thread.sleep(5000);
        t2.start();
    }
}
public class MyThread1 extends Thread {
    private CountDownLatch latch;
    public MyThread1(CountDownLatch latch){
        this.latch = latch;
    }
    @Override
    public void run() {
        System.out.println("A....");
        try {
            latch.await();//让当前线程等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C....");
    }
}
public class MyThread2 extends Thread {
    private CountDownLatch latch;

    public MyThread2(CountDownLatch latch){
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println("B....");
        //让latch的计数器减少1
        latch.countDown();
    }
}

5.CyclicBarrier

让多个线程,都到达了某种要求之后,新的任务才能执行

CyclicBarrier 的字面意思是可循环使用( Cyclic )的屏障( Barrier )。它要做的事情是,让一组线程到达一个屏障
(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运
行。

构造方法:
public CyclicBarrier(int parties, Runnable barrierAction);
                    需要多少个线程     所有线程都满足要求了,执行的任务

成员方法:
public int await();当某个线程达到了,需要调用await()

需求: 部门开会,假设部门有五个人,五个人都到达了才执行开会这个任务
    
public class TestPersonThread {
    public static void main(String[] args) throws InterruptedException {
        //0.创建一个CyclicBarrier
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("人都齐了,开会吧");
            }
        });

        //1.创建五个线程
        PersonThread p1 = new PersonThread(barrier);
        PersonThread p2 = new PersonThread(barrier);
        PersonThread p3 = new PersonThread(barrier);
        PersonThread p4 = new PersonThread(barrier);
        PersonThread p5 = new PersonThread(barrier);
        //2.开启
        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();
        //Thread.sleep(6000);
        //System.out.println("人都到了,开会吧...");
        //要求,人没到不开会,都到了立刻开会!!!
    }
}

public class PersonThread extends Thread {
    private CyclicBarrier barrier;
    public PersonThread(CyclicBarrier barrier){
        this.barrier = barrier;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(new Random().nextInt(6)*1000);
            System.out.println(Thread.currentThread().getName() + " 到了! ");
            //调用 barrier的await 表示线程到了
            try {
                barrier.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

补充:
	Math的静态方法
        public static double random(); //获取一个0(包括)到1(不包括)的正小数

6.Semaphore

用于控制并发线程的数量!!

Semaphore 的主要作用是控制线程的并发数量。
synchronized 可以起到 " " 的作用,但某个时间段内,只能有一个线程允许执行。
Semaphore 可以设置同时允许几个线程执行。
Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
构造方法:
public Semaphore(int permits); //参数 permits 表示最多允许有多少个线程并发执行
成员方法:
public void acquire(); //获取线程的许可证
public void release(); //释放线程的许可证
public class MyThread extends Thread {
    private Semaphore semaphore;

    public MyThread(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run(){
        //从Semaphore获取线程的许可
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " 进入 时间=" + System.currentTimeMillis());
        try {
            Thread.sleep(100*new Random().nextInt(10));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 结束 时间=" + System.currentTimeMillis());
        //归还semaphore线程的许可
        semaphore.release();
    }
}

public class TestDemo {
    public static void main(String[] args) {
        //0.创建Semaphore
        Semaphore semaphore = new Semaphore(3);

        //最多的并发线程数量为1
        for (int i = 0; i < 10; i++) {
            new MyThread(semaphore).start();
        }
    }
}

7.Exchanger

用于线程间的数据交换

Exchanger (交换者)是一个用于线程间协作的工具类。 Exchanger 用于进行线程间的数据交换。
这两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执
exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对
方。
 

构造方法:
    public Exchanger<V>();
成员方法:
    public V exchange(V x);//参数为发给别的线程的数据,返回值别的线程发过来的数据

public class TestExchanger {
    public static void main(String[] args) throws InterruptedException {
        //0.创建一个线程间数据交互对象
        Exchanger<String> exchanger = new Exchanger<String>();

        //1.创建线程A
        ThreadA aThread = new ThreadA(exchanger);
        aThread.start();

        //休眠
        Thread.sleep(5000);

        ThreadC cThread = new ThreadC(exchanger);
        cThread.start();
    }
}

public class ThreadA extends Thread {
    private Exchanger<String> exchanger;

    public ThreadA(Exchanger<String> exchanger) {
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        System.out.println("线程A,要将礼物AAA,送给线程C...");
        //调用exchanger
        String result = null;
        try {
            result = exchanger.exchange("AAA");//阻塞
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程A,获取到线程C的礼物:"+result);
    }
}

public class ThreadC extends Thread {
    private Exchanger<String> exchanger;

    public ThreadC(Exchanger<String> exchanger) {
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        System.out.println("线程C,要将礼物CCC,送给线程A...");
        String result = null;
        try {
            result = exchanger.exchange("CCC");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程C,获取到线程A的礼物:"+result);
    }
}

其他:

使用多线程是为了程序效率的需求,所以不能通过不使用多线程的方式实现,而是应该使用线程同步技术解决线程安全问题;

线程安全问题必须是多个线程对同一个数据发生了修改操作;如果多个线程对同一个数据的操作是读取操作,没有修改操作,则一定不会出现线程安全问题

同步代码块并没有增加程序效率,反而降低了程序的效率。多线程情况下,遇到同步代码,多个线程会依次进入。

某公司组织年会,会议入场时有两个入口,在入场时每位员工都能获取一张双色球彩票,假设公司有100个员工, 利用多线程模拟年会入场过程,并分别统计每个入口入场的人数,以及每个员工拿到的彩票的号码。线程运行后打印 格式如下:

编号为: 2 的员工 从后门 入场! 拿到的双色球彩票号码是:[17, 24, 29, 30, 31, 32, 07]
编号为: 1 的员工 从后门 入场! 拿到的双色球彩票号码是:[06, 11, 14, 22, 29, 32, 15]
//.....
从后门入场的员工总共: 13 位员工
从前门入场的员工总共: 87 位员工

===

双色球的工具类

import java.util.Arrays;
import java.util.Random;

public class DoubleColorBallUtil{
    // 产生双色球的代码
    public static String create() {
        String[] red = {"01","02","03","04","05","06","07","08","09","10",
                "11","12","13","14","15","16","17","18","19","20","21","22","23",
                "24","25","26","27","28","29","30","31","32","33"};
        //创建蓝球
        String[] blue = "01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16".split(",");
        boolean[] used = new boolean[red.length];
        Random r = new Random();
        String[] all = new String[7];
        for(int i = 0;i<6;i++) {
            int idx;
            do {
                idx = r.nextInt(red.length);//0‐32
            } while (used[idx]);//如果使用了继续找下一个
            used[idx] = true;//标记使用了
            all[i] = red[idx];//取出一个未使用的红球
        }
        all[all.length-1] = blue[r.nextInt(blue.length)];
        Arrays.sort(all);
        return Arrays.toString(all);
    }
}

线程安全:

public class LuckThread implements Runnable {
   // 员工人数
   private int number = 100;

   public void run() {
      // 获得线程的名字
      String name = Thread.currentThread().getName();
      // 定义变量统计人数
      int count = 0;
      // 开始进场抽奖,
      while (true) {
         synchronized (this) {
            // 首先判断number,大于0才能抽奖
            if (number > 0) {
               // 使用工具类生成一个彩票号码给这个员工
               String dc = DoubleColorBallUtil.create();
               // 输出抽中的彩票号
               System.out.println("编号为: " + number + " 的员工 从"+name+"入场! 拿到的双色球彩票号码是: " + dc);
               // 进入一个员工,少一个员工
               number--;
               // 计数加一
               count ++;
               // 休眠
               try {
                  Thread.sleep(10);
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
            } else {
               // 抽奖完毕,大于前后门入场的员工人数
           
               System.out.println("从" + name + "入场的员工总共: " + count + " 位员工");
               break;
            }
         }
      }
   }
}

public class Demo01 {
    public static void main(String[] args) {
        // 创建抽奖任务,实现了Runnable接口
        LuckThread ld = new LuckThread ();
        // 在主线程中开启两个线程,表示前门和后门
        // 传入实现Runnable接口的实现类对象
        Thread t1 = new Thread(ld, "前门");
        Thread t2 = new Thread(ld, "后门");
        // 开启两个线程
    	
}
发布了141 篇原创文章 · 获赞 27 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/u010581811/article/details/104901444