JUC实际上是Java包的缩写:java.util.concurrent包
1.进程:一个程序,例如QQ.exe,进程是程序的集合,进程是CPU调度的基本单位。一个进程可以有多个线程,至少包含一个。Java默认有两个线程,一个是main线程,另外一个是GC线程。
2.线程:线程是依附于进程的,进程可以启动线程,一个进程可以有多个线程;比如QQ同时跟多人聊天;迅雷同时下载多个不同的文件,每个下载都是一个线程;如果主线程结束了,但是还有其他线程,那么进程也不会结束;
3.线程创建方式:继承Thread、实现Runnable、实现Callable; Runnable 没有返回值 效率相比Callable较低
4.并发:并发是指一个cpu在一个时间片里面轮换的执行进程,假的同时,实际上是一个执行一会儿。
5.并行:并行是多核cpu在同时间执行不同的任务,真正的同时。
6.wait和sleep的区别:
wait是Java中Object类中的方法,sleep是Thread类中的方法。
wait是会释放锁的,而sleep是不会释放锁的,sleep就像是一个人抱着锁睡觉,但是wait则是把锁放在旁边进行等待。
wait只能在同步代码块中使用,sleep可以在任何地方使用。
管程实际上就是一个监视器(Monitor),就是我们所说的锁,换句话说:锁就是监视器。监视器本身是一种同步的机制,保证同一时间只有一个线程访问特定的数据。
jvm同步基于进入和退出的,使用管程对象实现的,每个对象都会有一个Monitor对象,Monitor会随着java对象的创建而创建。
用户线程:平时所用到的线程,基本上都是用户线程。
守护线程:后台所用到的线程,比如垃圾回收机制(GC),当用户线程销毁,守护线程也会销毁。可以这样理解:人(用户线程)在塔在,人不在了,塔(守护线程)也就没了。
首先介绍一下多线程编程的一般步骤:
创建资源类,在资源类中创建属性和操作方法
在资源类操作方法
判断
干活
通知
创建多个线程,调用资源类的操作方法
Lock简介(来自官方文档):
Lock实现提供了比使用 synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。这里的condition是用来线程通讯的,相当于wait()和notify()。
Lock的基本使用方法(来自官方文档):
只需要在资源类里面,增加一个lock的成员变量,然后调用对应的上锁和解锁方法就能实现。
注意:上锁之后,业务代码建议放在try{}finally{}中,要保证不管业务实现与否,都要释放锁,不然会造成死锁
Lock l = ...; //创建一个lock锁
l.lock();//加锁
try {
// access the resource protected by this lock
} finally {
l.unlock();//解锁
}
Lock的三个实现类:
ReentrantLock:一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
ReentrantReadWriteLock.WriteLock:可重用写锁
ReentrantReadWriteLock.ReadLock:可重用读锁
可重入锁:
可以重复使用的锁。例如上厕所,一个人进入测试然后上锁,出来后解锁,另外一个人又进去上锁,然后出来解锁。。。这就叫可重入锁。
买票小案例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 这是静态资源类
*/
class Ticket{
//票的数量
private int number = 30;
//声明一个可重用锁
private final Lock lock = new ReentrantLock();
public void sale(){
lock.lock();//上锁,保证只有一个线程能操作
try {
if (number>0)
number--;
System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+number);
} finally {
lock.unlock();//在finally里面解锁,让其他线程能争抢锁
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();//创建一个资源类的对象
new Thread(()->{for (int i = 0; i < 10; i++) ticket.sale();},"AA").start();
new Thread(()->{for (int i = 0; i < 10; i++) ticket.sale();},"BB").start();
new Thread(()->{for (int i = 0; i < 10; i++) ticket.sale();},"CC").start();
}
}
运行结果:
问题:消费者和生产者问题,生产一个,则消费一个。对一个资源变量(初始为0)进行加一和减一的操作。一个线程加一,另一个线程减一。
如果不使用线程之间的通讯,则无法实现上诉功能,基本思路是:A线程加一之后,通知B线程来减一。B线程减一之后,通知A线程来加一。
两种实现方法:
使用synchronized锁配合Object类中的wait()和notify()实现
使用lock锁配合condition对象的await()和condition.signal()实现
condition对象的获取:
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();
这里使用第二种方式
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyData{
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void add() throws InterruptedException {
lock.lock();
try {
if (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"减少了--"+num);
condition.signalAll();
}finally {
lock.unlock();
}
}
public void sub() throws InterruptedException {
lock.lock();
try {
if (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+"减少了--"+num);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class PC {
public static void main(String[] args) {
MyData myData = new MyData();
//用于增加的线程
new Thread(()->{for (int i = 0; i < 10; i++) {
try {
myData.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
//用于减少的线程
new Thread(()->{for (int i = 0; i < 10; i++) {
try {
myData.sub();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
}
}
运行结果:
可以看到,是0101交替执行的
问题:这里只有两个线程,所以没事,但是如果是同时有4个线程来操作,两个线程加,两个线程减。这种情况则会出现一个问题——虚假唤醒的问题。
四个线程运行结果:
可以看到4个线程运行结果,并不是0101交替执行
产生原因:上诉例子中,假设A线程和C线程都是增加的线程,其中一个线程进入if判断之后,然后休眠了。
下次被唤醒的时候,则不会再进行if判断了,因为wait的机制是,在哪里休眠,就在哪里唤醒。所以下次唤醒的时候,不管num是否为0,都会继续执行后面的代码。
解决方案:通过Java官方文档的内容,要避免产生虚假唤醒问题,则需要将if判断换成while判断。下面代码来自官方文档:
//等待应总是发生在循环中,如下面的示例:
synchronized (obj) {
while ()
obj.wait(timeout);
... // Perform action appropriate to condition
}
问题:A线程唤醒B线程,B线程唤醒C线程,C线程唤醒A线程。
这种通讯并不是和以前一样,唤醒所有的线程或者是唤醒其中一个线程,而是唤醒指定的线程。
实现思路:
package com.wang.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//创建资源类
class ShareData{
//定义标志位
private int flag = 1;
private final Lock lock = new ReentrantLock();
//创建三个Condition
private final Condition c1 = lock.newCondition();
private final Condition c2 = lock.newCondition();
private final Condition c3 = lock.newCondition();
public void doItA() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag!=1){
c1.await();
}
System.out.println(Thread.currentThread().getName()+"--");
//通知
flag = 2;
c2.signal();
}finally {
lock.unlock();
}
}
public void doItB() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag!=2){
c2.await();
}
System.out.println(" "+Thread.currentThread().getName()+"--");
//通知
flag = 3;
c3.signal();
}finally {
lock.unlock();
}
}
public void doItC() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag!=3){
c3.await();
}
System.out.println(" "+" "+Thread.currentThread().getName()+"--");
//通知
flag = 1;
c1.signal();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
shareData.doItA();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
shareData.doItB();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
shareData.doItC();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"C").start();
}
}
运行结果
可以看到ABC三个线程是交替执行的
我们都知道ArrayList是线程不安全的,当我们用多个线程去操作ArrayList的时候,就会产生并发安全异常。
解决方案:
使用Vector
实际开发中并不建议使用,这个方法是java1.0时候的,其原理就是给其中的方法加上synchronized关键字。属于无脑解决线程安全问题。
实际效率不高
使用Collections工具类
使用其synchronizedList()方法返回一个线程安全的List
List list = Collections.synchronizedList(new ArrayList<>());
也不推荐使用
使用CopyOnWriteArrayList解决
这个类是JUC里面的类
原理是:写时复制技术,读的时候并发读,写的时候独立写。
首先读的时候是很多个线程一起读
但是写的时候,先复制一份跟之前的内容相等的集合,然后在新的集合开始写,写完之后再跟以前的进行合并
再读的时候,就是读的新的集合
好处是:既进行了并发读,又可以进行独立写,没有并发安全
//Java源码是这样的:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//上锁
try {
Object[] elements = getArray();
int len = elements.length;
//复制一份新的数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//向新的数组里面添加数据
newElements[len] = e;
//将新的数组变成被读的哪一个
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
同理:当我们用多个线程去操作HashSet的时候,就会产生并发安全异常。
解决方案:使用CopyOnWriteArraySet解决。
同理:当我们用多个线程去操作HashMap的时候,就会产生并发安全异常。
解决方案:使用ConcurrentHashMap解决。
公平锁和非公平锁
非公平锁:会造成线程被“饿死”的情况,可能出现就是一个线程把活都干了,其他线程没有做事情的情况。大致可以这样理解,非公平锁在线程释放锁之后,又继续参与抢锁,然后又抢到了锁。
公平锁就是线程都会拿到“活”干
两个锁的优缺点:
公平锁:阳光普照,各个线程都能分配到任务,效率相对较低。因为在执行任务的时候,公平锁会先判断一下是否有被占用,如果被占用则进行排队,如果没有再进去。
非公平锁:线程饿死,效率高。
可重入锁:
synchronized(隐式)和Lock(显示)都是可重入锁
介绍:可重入锁就是如果进入一个锁之后,里面还有锁,则可以随便进入,实际上就是加的同一把锁,开了一次锁就不用再开了。例如我回家了,大门有个锁,开锁之后进了大门,里面的卧室门就不需要开开锁了。
public class Demo1 {
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(Thread.currentThread().getName()+"--外层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"--中层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"--内层");
}
}
}
},"T1").start();
}
}
可以看到,锁里面还有锁,但是T1线程可以自由的进入
什么是死锁:
两个或者两个以上的进程在执行过程中,因为争夺资源而造成一种互相等待现象,如果没有外力干涉,他们无法继续执行下去。
造成死锁的原因:
系统资源不足
进程推进顺序不合适
资源分配不当
1 . 产生死锁的必要条件:
(1)互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
(2)请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 (4)环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
除了通过继承Thread类和实现Runnable接口,还有另外一种创建线程的方式。
通过Callable接口和Runnable的区别:
Callable可以实现线程在完成时,有返回的结果方法call()。
call()方法可以引发异常,而run方法不能
实现Callable接口,必须重写call方法
Callable创建线程:
实现Callable接口并且重写call方法
借助FutureTask创建线程
class MyThread2 implements Callable {
@Override
public Integer call() throws Exception {
return 200;
}
}
public class Demo1 {
public static void main(String[] args) {
//Runnable创建线程
new Thread(new MyThread1(),"AA").start();
//Callable创建线程
//1.借助FutureTask创建线程
FutureTask task = new FutureTask<>(new MyThread2());
//2.使用lam表达式
FutureTask task1 = new FutureTask<>(()->{
return 111;
});
new Thread(task,"BB").start();
new Thread(task1,"CC").start();
System.out.println(task.get());
}
}
通过FutureTask的get方法,能够拿到线程的返回值,如果没有则一直等待。
通过FutureTask的isDone方法,能够判断线程是否已经执行完毕。
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减一操作。还有一个await方法,这个方法表示一个等待,线程会阻塞,只有到计数器里面的数变为0的时候,才会继续下面的代码。
当一个或者多个线程调用await方法时,这些线程会阻塞
其他线程调用countDonw方法会将计数器减一
当计数器的值变为0的时候,被await阻塞的线程就会被唤醒,继续执行
案例:放学了,有3个同学还在办公室做作业,只有当所有同学做完作业,离开办公室之后,门卫才可以锁门,否则门卫只能一直处于等待状态。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//创建countDownLatch类来实现计数
CountDownLatch countDownLatch = new CountDownLatch(3);
//模拟有三个同学
for (int i = 1; i <=3; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"--出教室了");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
//主线程模拟门卫
System.out.println("我锁门了");
}
}
执行结果
作用:可以用来线程直接的通讯。
官方文档:一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
CyclicBarrier可以看作是循环阻塞的意思,在使用中CyclicBarrier的构造方法可以传入一个目标障碍数,每次执行CyclicBarrier一次,障碍数会增加,如果达到了目标障碍数,才会执行CyclicBarrier中await之后的代码。可以将CyclicBarrier理解为加一的操作
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("阻塞达到了七次后输出这句话");
});
for (int i = 0; i < 7; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"被阻塞了...");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"出阻塞了");
},String.valueOf(i)).start();
}
}
}
通过代码我们可以发现,只有其余线程,总共调用了7次await方法,上面定义在CyclicBarrier类中的代码才会执行;如果删掉cyclicBarrier.await(),则不会执行那句话。
当数量达到7次之后,被阻塞的7个线程又会从wait后面继续执行
官方文档:一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore
只对可用许可的号码进行计数,并采取相应的行动。
有多个线程执行任务,然后都要获得Semaphore的许可证,许可证可以有多个,只有拥有许可证的线程才能够继续执行后面的业务代码,没获得许可证的就不能继续执行,只能等待别人释放许可证。
public class SemaphoreDemo {
public static void main(String[] args) {
//创建一个信号灯类,然后给里面初始化三张许可证
Semaphore semaphore = new Semaphore(3);
//创建六个线程去争抢许可证
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
//抢占许可证
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了许可证");
//模拟三秒钟的业务时间
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放许可证
semaphore.release();
}
}).start();
}
}
}
运行结果:
只有三个线程抢到了许可证,剩余的线程则在等待,三秒后前三个释放许可证,后三个抢到许可证
首先先介绍几种锁:
悲观锁就是,线程在操作某个数据的时候,首先先上锁。然后别的线程只能阻塞。顾名思义,悲观锁很悲观,他认为我在操作数据的时候,一定有人来更改数据,所以一开始就上锁了。优点:能解决并发的各种问题;缺点:效率低;
乐观锁就是,线程在操作某个数据的时候,先不上锁,拿到该数据的版本号,等线程操作完之后判断版本号是否有改变,如果没有改变,则操作成功,如果失败则操作失败。操作成功更改版本好
乐观锁,就很乐观,认为我在操作数据的时候,不会有人来动我的数据,所以我第一时间并不上锁,等我操作完毕要提交的时候,再判断一下有人改过我数据了吗,如果没有改过则操作成功,反之操作失败。
表锁就是,假设对数据库的一张表进行操作,如果线程对一张表里面的一条记录进行操作,但是把整个表都锁了,这就是表锁。表锁不会发生死锁。
操作一条记录只对该记录对应的行上锁,就是行锁。行锁可能会发生死锁
读锁:共享锁,发生死锁。
写锁:独占锁,发生死锁
java通过ReentrantReadWriteLock来获得读写锁
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCatch{
private final Map map = new HashMap<>();
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
public void put(String key,Integer value) {
rw.writeLock().lock();//写锁上锁
try {
System.out.println(Thread.currentThread().getName()+"正在写操作"+key);
TimeUnit.SECONDS.sleep(3);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rw.writeLock().unlock();//写锁解锁
}
}
public int get(String key) {
rw.readLock().lock();//读锁上锁
int integer = -1;
try {
System.out.println(Thread.currentThread().getName()+"正在读操作"+key);
TimeUnit.SECONDS.sleep(3);
integer = map.get(key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rw.readLock().unlock();//读锁解锁
}
return integer;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCatch myCatch = new MyCatch();
for (int i = 0; i < 5; i++) {
int num = i;
new Thread(()->{
myCatch.put(num+"",num);
}).start();
}
for (int i = 0; i < 5; i++) {
int num = i;
new Thread(()->{
myCatch.get(num+"");
}).start();
}
}
}
运行结果:
可以看到,写锁是一个一个去写的,一个线程在写的时候,别人不能进入。
但是读锁是一起读的。
读写锁:一个资源可以被多个读的线程访问,或者可以被一个写的线程访问,但是不能同时存在读写操作,读写是互斥的,读读是共享的。
演变:
锁降级:
将写入锁降级为读锁