目录
1 多线程
1.1 基本概念
1.2 创建线程的三种方式
1.4 解决线程安全问题的三种方法
1.5 线程通信
1.6 线程状态
2 线程池
2.1线程池的概念
2.2 创建并提交任务
3 可见性
3.1 变量不可见性
3.2 变量不可见性的解决方案
4 原子性
4.1 原子性的概念
4.2 保证原子性的方案
4.3 原子类的CAS机制
5 多线程的并发包
5.1 ConcurrentHashMap类
5.2 CountDownLatch类
5.3 CyclicBarrier类
5.4 Semaphore类
5.5 Exchanger类
1 多线程
1.1 基本概念
程序(program):为了完成特定的任务,使用某种语言编写的一组指令的集合,也就是一段静态的代码。
进程(process):程序加载到内存中的一次执行过程,或者是正在运行中的一个程序。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread):一个进程可被进一步细化成一个或多个线程,线程就是一个程序内部的一条执行路径。如果一个程序可以同时并行执行多个线程,我们就称它是支持多线程的。线程作为调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器,所有的线程共享进程分配的堆和方法区,都从同一个堆中分配对象访问相同的变量和对象。这就是的线程之间的通信更加简便、高效,但是由于共享系统资源也就带来了安全隐患。
单CPU与并发:CPU相当于人的大脑,用来动态的为程序的运行分配内存空间,之所以是动态的是因为一个CPU一次只能执行一个进程,但是同一时间一台电脑几乎不可能只开启一个进程,一个CPU会不停的切换执行多个进程也就是并发执行,由于切换的速度比较快在人类看来计算机就是在同时执行多个进程。
多CPU与并行:多CPU是相对于单CPU而言的概念,多CPU就是多个CPU同时执行不同的进程也就是并行执行,与此同时每个CPU还会不停的切换执行多个进程也就是并发执行。
并发与并行:举个例子,比如说今年暑假的抗洪救灾现场,需要将装成袋的沙子搬到决堤口挡水,并发:这里有20袋沙子(相当于20个进程),但是只有一个人来搬(单CPU),这个人搬完一袋换一袋由于换的速度比较快,看起来就好像是20袋沙子一块被搬一样。并发和并行同步执行:这里有20袋沙子(相当于20个进程),但是有四个人来搬(多CPU),四个人同时搬就是并行,这四个人搬完各自的一袋换一袋由于换的速度比较快看起来也好像是20袋沙子一块被搬一样,这里的每个人搬完换另一袋就叫并发。于是大部分情况下的单CPU的性能要优于多CPU的。
一个java应用程序java.exe至少应该包三个线程:main()主线程、gc()垃圾回收线程、异常处理线程。
1.2 创建线程的三种方式
方法一:继承Thread类
四步:创建类并继承Thread-->重写run方法-->创建线程对象-->调用start方法
⚠ 创建线程对象调用start方法才会产生新的线程(start方法底层会先向CPU注册线程,在调用run方法),如果调用run方法会被当做是一个普通类执行,这样进程里面也就还只有一个主线程。
⚠ main方法里面要先创建子线程出来再分配主线程的任务,否则在进程执行的时候会认为只有一个主线程,因为从代码的执行顺序来看此时还没有创建子线程,从而会导致永远都是先执行完主线程任务再执行子线程任务。
这样创建线程的优点是编码简单,缺点是通过继承Thread类创建线程会导致线程类无法在对其他类进行继承,功能无法通过继承来拓展(单继承的局限性)
Thread的常用API
方法二:实现Runnable接口
五步:创建任务类并实现Runnable接口-->重写run方法-->创建任务对象-->将任务对象包装成线程对象-->调用start方法
这个方法创建线程的缺点:比上一种方法多了一步,下一个方法可以获取重新写call方法的返回结果而这个的run方法没有返回值。优点有:由于任务类没有继承任何类,可以继续继承其他类拓展功能;同一个任务类可以被包装成多个线程对象;适合多个线程共享同一个资源;实现解耦操作,任务可以被多个线程共享,任务与任务之间有相互独立不影响
创建线程的简化写法(匿名内部类)
方法三:实现Callable接口
六步:创建任务类并实现Callable接口-->重写call方法-->创建任务对象-->将任务对象包装成FutureTask对象-->将FutureTask对象包装成线程对象-->调用start方法
第三种方法和第二种方法的差别就是这个方法可以获取返回值
1.3 线程安全问题
当多个线程操作同一个共享资源的时候就有可能会出现线程安全问题。比如说,小明和小红有一个共同情侣账户里面有100块钱,小明和小红同时登录系统取钱,会出现以下情况:
线程号 | 人员 | 操作 | 结果 | 账户余额 |
1 | 小明 | 查询余额>=100? | true | 100 |
2 | 小红 | 查询余额>=100? | true | 100 |
3 | 小明 | 取100 | 100-100 | 0 |
4 | 小红 | 取100 | 0-100 | -100 |
由于线程的执行时随机且无法回退的,所以可能会导致两人都查询账户余额有100块的情况,线程继续往后执行就会导致账户被两次取钱成为负值,这肯定是有问题的。
账户bean类:
主类:
取钱任务类:
控制台运行结果:
1.4 解决线程安全问题的三种方法
方法一:同步代码块
synchronized(锁对象) {
访问共享资源的核心代码;
}
⚠ 在实例方法中建议使用this作为锁对象,静态方法中建议使用类名.class作为锁对象
方法二:同步方法
在方法的定义时使用synchronized修饰即可
同步方法与同步代码块的方法差不多,同步方法的底层是将整个方法都锁了起来
方法三:Lock显式锁
创建锁对象:
上锁:
解锁:
使用该方法上锁的话,尽量要按照这种try-catch-finally的方式,否则可能遇到上锁之后出现异常,此时程序就无法继续运行,也就是说永远无法解锁导致出现问题。
1.5 线程通信
现在有这么一个需求
使用IDEA进行实现代码:
账户bean类:
主类:
取钱任务类:
存钱任务类:
控制台运行结果:
这是个死循环运行了一会就暂停了截图
1.6 线程状态
⚠ sleep方法只是计时等待,不会把锁放开;wait方法是把锁放开进入等待。
死锁:
死锁就是不同的线程同时分别占用着对方需要的锁不放,都在等待着对方放锁。出现死锁之后不会产生任何的异常和提示,只是所有的线程都处于阻塞状态无法继续。
死锁产生的四个必要条件:
- 互斥使用:即共享资源一次只能被一个线程使用
- 不可抢占:即线程不能从正在使用共享资源的线程手中夺取资源
- 请求保持:一个线程在请求另一个线程资源的同时依然占有着那个线程所请求的资源
- 循环等待:1要2的资源,2要1的资源,形成了一个循环等待
2 线程池
2.1线程池的概念
前面讲过,每当我们需要使用线程的时候,不管是使用哪个方法都需要去创建一个线程,实现起来并不难但是会产生一个问题:如果并发的线程数量很多且线程的执行时间都很短的时候,线程的创建和销毁都需要时间,频繁的创建销毁线程就会导致系统的效率大大降低。
解决以上问题就用到了线程池的概念,线程池就是一个可以容纳固定多个线程的容器,线程池中的线程可以反复使用。线程池中工作线程(PoolWorker)的个数是固定的,而任务接口(Task)想要使用工作线程的话就需要在任务队列(TaskQueue)中排队等待,任务执行完毕之后工作线程归还线程池出于空闲状态。
2.2 创建并提交任务
创建线程池并指定线程数量:
无返回值的Runnable任务:
有返回值的Callable任务:
⚠ 线程池对象调用submit(任务对象)方法将任务对象提交给线程池执行,线程池在执行完所有的任务之后并不会直接关闭,而是处于等待状态等待其他任务的使用,如果没有其他任务就一直处于等待状态,可以调用shutdown()方法等待任务执行完毕之后关闭线程池。
3 可见性
3.1 变量不可见性
首先,Java专门为多线程定义了一种Java内存模型(Java Memory Model JMM),这种内存模型要不同于单线程的内存模型JVM。JMM描述了Java程序中各种共享变量的访问规则,以及在JVM中将变零存储在内存中和从内存中读取像变量的底层细节。
JMM的规定:
- 所有的共享变量都存储于主内存。这里的变量指的是实例变量和类变量,并不包含局部变量,因为局部变量是线程私有的不存在竞争问题。
- 每个线程有自己的工作内存,里面存放的是从主内存中拷贝来的共享变量副本。
- 线程对变量的所有操作都在线程的工作内存中完成,而不是直接操作主内存中的共享变量。
- 不同线程之间也不能访问对方的工作内存,线程间变量的值传递通过主内存中转完成。
不可见性描述:
并发编程下,也就是说当存在多个线程访问一个共享资源时,一个线程改变了这个资源的变量值,但是其他线程并不能看到这个变量值的改变,读取到的依然是变量修改之前的值。以上现象又被称为是多线程间变量的不可见性
变量不可见性的原理:
3.2 变量不可见性的解决方案
方案一:加锁
对线程任务进行加锁。其底层原理在于:线程在获得锁对象之后会清空线程的工作内存,从主内存中再次拷贝共享变量的值成为共享变量副本,此时线程工作内存中共享变量副本就是最新的变量值了。
方案二:volatile关键字修饰
定义变量的时候使用volatile关键字进行修饰。其底层原理与加锁不同的是:volatile关键字是在主内存发现有线程对共享变量的值进行修改之后,通知其他线程工作内存中的共享变量副本的值失效,其他线程在访问共享变量的副本的时候发现值已失效,于是重新拷贝共享变量至工作内存中。
4 原子性
4.1 原子性的概念
原子性指的是:一批操作是一个整体,要么同时成功要么同时失败,不能被其他干扰。volatile只能保证线程之间变量的可见性,但是不能保证变量操作的原子性。
4.2 保证原子性的方案
方案一:加锁
加锁就是对线程任务进行加锁。加锁不仅能够保证线程的原子性,还能保证线程之间变量值修改的可见性。但是加锁会降低程序的性能,故又有了第二种方法。
方案二:原子类
Java提供了java.util.concurrent.atomic包(简称atomic包),包里面有各种类类中有很多方法。
原子类包含有很多种,其中包括AtomicInteger、AtomicDouble……对不同数据类型的数据进行操作更新的类,这些类中定义了一些API去代替运算,与普通运算方式的区别在于,这种方法的运算能在不加锁的情况下保证线程的原子性。
原子类的使用:
原子类中定义了很多的API根据自己的需求选择使用
从上图中可以看出来,一个线程执行完所有的任务下一线程再执行,这种模式与前面的上锁很像,其实原子类就是加锁机制的高性能版本,在实现加锁机制保证线程安全的同时又保证了原子性。
4.3 原子类的CAS机制
CAS的全称为Compare And Swap译为先比较再交换,CAS可以将read-modify-check-write操作转换为原子操作,保证了线程的原子性。CAS机制不锁任务,任意线程的任何时候都可以操作任务,就是操作完任务之后要将操作前的共享变量副本与主内存中的共享变量值进行对比,一致的话就修改主内存中共享变量的值,不一致的话就将之前的任务操作作废,重新开启一次任务(拷贝、修改、对比)。
5 多线程的并发包
5.1 ConcurrentHashMap类
java.util.concurrent.ConcurrentHashMap
在创建HashMap集合的时候使用即可,创建之后即可保证线程安全,类下面的API操作和HashMap一样,正常使用即可。
5.2 CountDownLatch类
java.util.concurrent.CountDownLatch
创建一个计数器,用于实现线程执行时的计数等待,使用有参构造创建对象的同时给定计数步数,也就是说等待计数器减几次,await()方法让当前线程让出CPU等待计数器的值清零,countDown()方法可以将计数器的值减1
5.3 CyclicBarrier类
java.util.concurrent.CyclicBarrier
创建一个循环屏障对象,传入两个参数阻挡线程个数和一个Runnable任务对象,意思就是屏障阻挡了相应的线程个数之后就执行这个Runnable任务
5.4 Semaphore类
java.util.concurrent.Semaphore
Semaphore对象的主要作用就是控制线程并发的数量,也就是说使用有参构造创建一个Semaphore对象设置最大允许进入acquire()方法和release()方法之间任务代码的线程个数。可以用来限制一个资源同一时间的的最大访问人数,使用synchronized上锁相当于创建Semaphore对象的时候传参为1。
5.5 Exchanger类
java.util.concurrent.Exchanger
Exchanger类适用于线程间协作通信的类,利用构造器定义一个Exchanger对象容器使用泛型规定容器暂存数据的类型,可以是无参构造器也可以是有参构造器,两个参数超时不再交换时间和超时时间单位,调用exchange(V x)方法进行数据交换并返回交换之后对方传过来的结果。