❤你还怕多线程吗?全网最全多线程笔记(下)❤

举报
XiaoLin_Java 发表于 2021/08/11 21:00:49 2021/08/11
【摘要】 大家好,我是会写Bug又会Rap的XiaoLin。遇事先百度,学习关注我,今天我们来学学多线程 八、并发包​    在实际开发中如果不需要考虑线程安全问题,大家不需要做线程安全,因为如果做了反而性能不好!但是开发中有很多业务是需要考虑线程安全问题的,此时就必须考虑了。否则业务出现问题。​    Java为很多业务场景提供了性能优异,且线程安全的并发包,程序员可以选择使用! 8.1、Concu...

BugRapXiaoLin线

八、并发包

​    在实际开发中如果不需要考虑线程安全问题,大家不需要做线程安全,因为如果做了反而性能不好!但是开发中有很多业务是需要考虑线程安全问题的,此时就必须考虑了。否则业务出现问题。
​    Java为很多业务场景提供了性能优异,且线程安全的并发包,程序员可以选择使用!

8.1、ConcurrentHashMap

​    Map集合中的经典集合:HashMap它是线程不安全的,性能好。如果在要求线程安全的业务情况下就不能用这个集合做Map集合,否则业务会崩溃。

​    为了保证线程安全,可以使用Hashtable。Hashtable是线程安全的Map集合,但是性能较差!(已经被淘汰了,虽然安全,但是性能差)

​    为什么说HashTable的性能差呢?我们看看源码可以得知,HashTable的每一个方法都用synchronized修饰了,实在是过于悲观。

​    ConcurrentHashMap不止线程安全,而且效率高,性能好,最新最好用的线程安全的Map集。

8.1.1、HashMap线程不安全演示

public class Const {
	public static HashMap<String,String> map = new HashMap<>();
}
public void run() {
		for (int i = 0; i < 500000; i++) {
			Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
		}
    	System.out.println(this.getName() + " 结束!");
	}
public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Thread1A a1 = new Thread1A();
		Thread1A a2 = new Thread1A();
		a1.setName("线程1-");
		a2.setName("线程2-");

		a1.start();
		a2.start();
		//休息10秒,确保两个线程执行完毕
		Thread.sleep(1000 * 5);
		//打印集合大小
		System.out.println("Map大小:" + Const.map.size());
	}
}

​    我们执行后可以发现出来的错误是有以下三种:

  1. 没有达到预期的效果

在这里插入图片描述

  1. 抛出异常

在这里插入图片描述

  1. 结果错误

在这里插入图片描述

8.1.2、Hashtable演示

public class Const {
	public static Hashtable<String,String> map = new Hashtable<>();
}
public void run() {
		long start = System.currentTimeMillis();
		for (int i = 0; i < 500000; i++) {
			Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
		}
		long end = System.currentTimeMillis();
		System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒");
	}
public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Thread1A a1 = new Thread1A();
		Thread1A a2 = new Thread1A();
		a1.setName("线程1-");
		a2.setName("线程2-");

		a1.start();
		a2.start();
		//休息10秒,确保两个线程执行完毕
		Thread.sleep(1000 * 5);
		//打印集合大小
		System.out.println("Map大小:" + Const.map.size());
	}
}

在这里插入图片描述

8.1.3、ConcurrentHashMap演示

public class Const {
	public static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
}
public void run() {
		long start = System.currentTimeMillis();
		for (int i = 0; i < 500000; i++) {
			Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
		}
		long end = System.currentTimeMillis();
		System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒");
	}
public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Thread1A a1 = new Thread1A();
		Thread1A a2 = new Thread1A();
		a1.setName("线程1-");
		a2.setName("线程2-");

		a1.start();
		a2.start();
		//休息10秒,确保两个线程执行完毕
		Thread.sleep(1000 * 5);
		//打印集合大小
		System.out.println("Map大小:" + Const.map.size());
	}
}

在这里插入图片描述

8.1.4、HashTable效率低下的原因

    我们查看HashTable的源码我们可以发现他的每一个方法都用synchronized修饰了,实在是过于悲观。

public synchronized V put(K key, V value) 
public synchronized V get(Object key)

    在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

在这里插入图片描述

8.1.5、ConcurrentHashMap高效的原因

    ConcurrentHashMap上锁机制:CAS + 局部(synchronized)锁定(分段式锁)

在这里插入图片描述

8.1.6、总结

  1. HashMap是线程不安全的。
  2. ​ Hashtable线程安全基于synchronized,综合性能差,被淘汰了。
  3. ​ ConcurrentHashMap:线程安全的,分段式锁,综合性能最好,线程安全开发中推荐使用

8.2、CountDownLatch

    CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。

需求

    线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造器以及方法

 public CountDownLatch(int count)// 初始化唤醒需要的down几步。count相当于一个计数器
方法 详解
public void await() throws InterruptedException 让当前线程等待,必须down完初始化的数字才可以被唤醒,否则进入无限等待
public void countDown() 计数器进行减1 (down 1)

8.2.1、例子

public class ThreadA extends Thread {
    private CountDownLatch down ;
    public ThreadA(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("A");
        try {
            down.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C");
    }
}
public class ThreadB extends Thread {
    private CountDownLatch down ;
    public ThreadB(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("B");
        down.countDown();
    }
}
public class Demo {
    public static void main(String[] args) {
        CountDownLatch down = new CountDownLatch(1);//创建1个计数器
        new ThreadA(down).start();
        new ThreadB(down).start();
    }
}

8.2.2、总结

  1. CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。
  2. CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch的wait()方法的线程阻塞状态解除,继续执行。

8.3、CyclicBarrier

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

​ CyclicBarrier构造方法:

public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景

​ CyclicBarrier重要方法:

public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞

8.3.1、例子

制作员工线程

public class PersonThread extends Thread {
	private CyclicBarrier cbRef;
	public PersonThread(CyclicBarrier cbRef) {
		this.cbRef = cbRef;
	}
	@Override
	public void run() {
		try {
			Thread.sleep((int) (Math.random() * 1000));
			System.out.println(Thread.currentThread().getName() + " 到了! ");
			cbRef.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}

制作开会线程

public class MeetingThread extends Thread {
    @Override
    public void run() {
        System.out.println("好了,人都到了,开始开会......");
    }
}

制作测试类

public class Demo {
	public static void main(String[] args) {
		CyclicBarrier cbRef = new CyclicBarrier(5, new MeetingThread());//等待5个线程执行完毕,再执行MeetingThread
		PersonThread p1 = new PersonThread(cbRef);
		PersonThread p2 = new PersonThread(cbRef);
		PersonThread p3 = new PersonThread(cbRef);
		PersonThread p4 = new PersonThread(cbRef);
		PersonThread p5 = new PersonThread(cbRef);
		p1.start();
		p2.start();
		p3.start();
		p4.start();
		p5.start();
	}
}

8.3.2、使用场景

    CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

8.4、Semaphore

    Semaphore(发信号)的主要作用是控制线程的并发数量。他的机制和synchronized一样都是上锁,但是,但某个时间段内,synchronized只能有一个线程允许执行。Semaphore可以设置同时允许几个线程执行。它的作用是控制访问特定资源的线程数目。

    Semaphore构造方法:

public Semaphore(int permits)						//permits 表示许可线程的数量
public Semaphore(int permits, boolean fair)			//fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

    Semaphore重要方法:

public void acquire() throws InterruptedException	//表示获取许可
public void release()								//release() 表示释放许可

8.4.1、示范一

    我们测试一下只允许一个线程的案例。

制作一个Service类

public class Service {
    private Semaphore semaphore = new Semaphore(1);//1表示许可的意思,表示最多允许1个线程执行acquire()和release()之间的内容
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()
                    + "   结束 时间=" + System.currentTimeMillis());
            semaphore.release();
			//acquire()和release()方法之间的代码为"同步代码"
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

制作线程类

public class ThreadA extends Thread {
	private Service service;
	public ThreadA(Service service) {
		super();
		this.service = service;
	}
	@Override
	public void run() {
		service.testMethod();
	}
}

测试类

public class Demo {
	public static void main(String[] args) {
		Service service = new Service();
        //启动5个线程
		for (int i = 1; i <= 5; i++) {
			ThreadA a = new ThreadA(service);
			a.setName("线程 " + i);
			a.start();//5个线程会同时执行Service的testMethod方法,而某个时间段只能有1个线程执行
		}
	}
}

在这里插入图片描述

8.4.2、示范二

    我们测试一下只允许两个线程的案例。

修改Service类

public class Service {
    private Semaphore semaphore = new Semaphore(2);//2表示许可的意思,表示最多允许2个线程执行acquire()和release()之间的内容
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName()
                    + "   结束 时间=" + System.currentTimeMillis());
            semaphore.release();
			//acquire()和release()方法之间的代码为"同步代码"
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

8.5、Exchanger

    Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

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

    Exchanger构造方法:

public Exchanger();

    Exchanger重要方法:

public V exchange(V x)

8.5.1、exchange方法的阻塞特性

  1. 制作线程A,并能够接收一个Exchanger对象:
public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
			System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));

		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}
  1. 制作main()方法
public class Demo {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

在这里插入图片描述

8.5.2、exchange方法执行交换

  1. 制作线程A
public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
			System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
  1. 制作线程B
public class ThreadB extends Thread {
	private Exchanger<String> exchanger;
	public ThreadB(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程B欲传递值'礼物B'给线程A,并等待线程A的值...");
			System.out.println("在线程B中得到线程A的值=" + exchanger.exchange("礼物B"));

		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
  1. 制作测试类
public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		ThreadB b = new ThreadB(exchanger);
		a.start();
		b.start();
	}
}

在这里插入图片描述

8.5.3、exchange方法超时

    exchange方法我们可以设置不一直等待,可以设置一个超时时间。

  1. 制作线程A
public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值,只等5秒...");
			System.out.println("在线程A中得到线程B的值 =" + exchanger.exchange("礼物A",5, TimeUnit.SECONDS));
			System.out.println("线程A结束!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.out.println("5秒钟没等到线程B的值,线程A结束!");
		}
	}
}
  1. 制作测试类
public class Run {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

在这里插入图片描述

8.5.4、使用场景

    我们可以做可以做数据校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,并对两个文件数据进行校对,看看是否录入一致。

九、线程池

    我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

    如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。

    于是Java提供了一种思想:线程池,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。

9.1、线程池的概念

    线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

在这里插入图片描述

    合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

9.2、线程池的实现

    Java中线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具。真正的线程池接java.util.concurrent.ExecutorService

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

    Executors类中有个创建线程池的方法有:

  1. public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

  2. public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。

    使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。
  3. 提交Runnable接口子类对象。
  4. 关闭线程池(一般不做)。

9.2.1、Runnable

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个教练");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();

        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 调用MyRunnable中的run()

        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        //service.shutdown();
    }
}

9.2.2、Callable

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果。

  • V get() : 获取计算完成的结果。

public class ThreadPoolDemo2 {
    public static void main(String[] args) throws Exception {
        // 创建线程池对象
      ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象

        // 创建Runnable实例对象
        Callable<Double> c = new Callable<Double>() {
            @Override
            public Double call() throws Exception {
                return Math.random();
            }
        };

        // 从线程池中获取线程对象,然后调用Callable中的call()
        Future<Double> f1 = service.submit(c);
        // Futur 调用get() 获取运算结果
        System.out.println(f1.get());

        Future<Double> f2 = service.submit(c);
        System.out.println(f2.get());

        Future<Double> f3 = service.submit(c);
        System.out.println(f3.get());
    }
}

十、死锁

    多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

    举个例子:客户(占用资金,等待经销商的货品资源) 经销商(占用货品资源,等待客户的资金)

10.1、死锁产生的必要条件

  1. 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  2. 不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路

    当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失

10.2、死锁代码实现

    死锁是多个线程满足上述四个条件才会形成,死锁需要尽量避免,且死锁一般存在资源的嵌套请求!

package test;

/**
 * @author Xiao_Lin
 * @date 2020/12/30 19:10
 */
public class DeadlockDemo {
    // 1.至少需要两个资源,每个资源只需要1份。
  public static Object resources1 = new Object();
  public static Object resources2 = new Object();;

  public static void main(String[] args) {
    new Thread(new Runnable() {
      @Override
      public void run() {
           // 线程1:占用资源1 ,请求资源2
        synchronized (resources1){
          System.out.println("线程1抢占资源1,等待资源2");
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          synchronized (resources2){
            System.out.println("线程1成功抢占资源2");
          }
        };

      }
    }).start();

    new Thread(new Runnable() {
      @Override
      public void run() {
        synchronized (resources2){
          System.out.println("线程2抢占资源2,等待资源1");
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          synchronized (resources1){
            System.out.println("线程2成功抢占资源1");
          }
        }

      }
    }).start();
  }
}

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。