文章中标明星号的地方说明是需要重点掌握的
一、juc基础知识
1. 什么是juc
java.util.concurrent (juc是包名的简写)在并发编程中使用的工具类,是关于并发编程的API。
2. 线程和进程
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。
- 线程上下文切换比进程上下文切换要快得多。
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源,某进程内的线程在其他进程不可见。
- java默认线程有2个。(main线程、GC线程)
问:java可以开启线程吗? 答:不能!!只能通过本地方法进行调用 理由如下: 进入start()方法: start0是一个本地方法,底层是C++,java无法直接操作
3. 并发和并行的区别
并发:一个处理器同时处理多个任务。同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的。
并行:多个处理器或者是多核的处理器同时处理多个不同的任务。同一时刻,有多条指令在多个处理器上同时执行。
并发编程的本质:充分利用CPU的资源
4. 线程的六种状态
- NEW :线程刚创建完但未启动
- RUNNABLE :线程运行
- BLOCKED :线程阻塞
- WAITING :线程等待
- TIMED_WAITING :线程超时等待
- TERMINATED :线程执行完成
问:wait()和sleep()的区别
- wait()来自Object类,sleep()来自Thread类。
- wait()会释放锁,sleep()不会释放锁。
- wait()只能在同步代码块中使用,sleep()可以在任何地方使用。
- wait()不需要捕获异常,sleep()必须要捕获异常。
二*、 Lock锁
Lock是一个接口。 公平锁:先来后到。加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程。 非公平锁:线程加锁时直接尝试获取锁(线程间竞争锁),获取不到就自动到队尾等待。 默认非公平锁。 代码示例:
lock.lock() 最规范的写法是写到 try 语句的外面。 若要写在里面,请写到try语句块的第一行,防止try中加锁前的代码出现异常。 因为如果采用Lock,必须主动去释放锁。而且在发生异常时,也不会自动释放锁。
ReentrantLock和synchronized都是JDK提供的可重入的锁。
什么是可重入锁呢?
1. Synchronized和Lock锁的区别
- Synchronized是java内置关键字,Lock是一个java类
- Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁。
- Synchronized会自动释放锁,Lock需要手动释放,若不释放会造成死锁。
- 使用Synchronized时,一个线程1获得了锁,另一个尝试获取锁的线程2会等待线程1释放锁,若线程1阻塞了那线程2会一直等待。使用Lock锁时,线程2就不一定会一直等待下去。
boolean isLocked = lock.tryLock();
lock.tryLock(10, TimeUnit.SECONDS);
- Synchronized是可重入锁,且是不可中断的非公平锁。(由于Synchronized是关键字所以不能修改);Lock也是可重入锁,可以判断锁的状态,可以设置是否为公平锁。
如果线程A正在执行锁中的代码,线程B正在等待获取该锁,由于等待时间过长,线程B不想等待了,我们可以让它自己中断自己或者在别的线程中中断它,这种就是可中断锁。
- Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
相关高频问题-----生产者消费者问题中出现的虚假唤醒问题,详见JUC—线程间定制化通信
2. Condition详解
Condition用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
Condition依赖于Lock接口:
Condition condition = lock.newCondition();
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock()之间才可以使用。
condition.await();
condition.signalAll();
condition.signal();
在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。传统线程的通信方式,Condition都可以实现。
Condition的优势在于他可以精准的通知和唤醒线程。 代码如下:juc版本的生产者消费者问题
package com.jess;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class demo2 {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for (int i = 0; i < 2; i++) {
resource.printA();
}
},"A线程").start();
new Thread(()->{
for (int i = 0; i < 2; i++) {
resource.printB();
}
},"B线程").start();
new Thread(()->{
for (int i = 0; i < 2; i++) {
resource.printC();
}
},"C线程").start();
}
}
class Resource{
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1;
public void printA(){
lock.lock();
try{
while(number !=1){
condition1.await();
}
System.out.println("A线程执行中......");
number = 2;
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try{
while(number != 2){
condition2.await();
}
System.out.println("B线程执行中......");
number = 3;
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try{
while(number != 3){
condition3.await();
}
System.out.println("C线程执行中......");
number = 1;
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
三*、八锁现象
八锁就是关于锁的八个问题
public class demo3 {
public static void main(String[] args) {
Fruit fruit = new Fruit();
new Thread(()->{
fruit.apple();
},"A线程").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
fruit.banana();
},"B线程").start();
}
}
class Fruit{
public synchronized void apple(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("吃苹果");
}
public synchronized void banana(){
System.out.println("吃香蕉");
}
}
问题1.标准情况下,两个线程哪个先运行? 问题2.apple()方法延迟4s后,两个线程哪个先运行? 答:都是线程A先执行。因为有锁的存在。synchronized锁的对象是方法的调用者,所以banana()和apple()方法都是由fruit对象调用的,二者用的同一把锁,谁先拿到锁就是谁先执行。
public void grape(){
System.out.println("吃葡萄");
}
public static void main(String[] args) {
Fruit fruit = new Fruit();
new Thread(()->{
fruit.apple();
},"A线程").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
fruit.grape();
},"B线程").start();
}
问题3.在新增grape()方法后,A、B线程谁先执行? 答:B线程先执行,即输出“吃葡萄”。grape()方法没有加锁,不是同步方法,不受锁的影响。
public static void main(String[] args) {
Fruit fruit1 = new Fruit();
Fruit fruit2 = new Fruit();
new Thread(()->{
fruit1.apple();
},"A线程").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
fruit2.banana();
},"B线程").start();
}
问题4.两个对象,分别调用两个同步方法banana()和apple(),哪一个先执行? 答:banana()先执行。synchronized锁的对象不同,有两把锁,所以按照时间顺序执行。(apple()方法延迟了4s)
class Fruit{
public static synchronized void apple(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("吃苹果");
}
public static synchronized void banana(){
System.out.println("吃香蕉");
}
}
问题5.apple()和banana()为静态方法,哪一个线程先执行? 答:apple()先执行。static方法在类加载的时候就已经加载进内存了,锁的对象是类的class对象,Fruit类只有唯一的一个class对象,所以两个方法使用的同一把锁。
Fruit fruit1 = new Fruit();
Fruit fruit2 = new Fruit();
fruit1.apple();
fruit2.banana();
问题6.两个fruit对象调用apple()和banana()静态方法,哪一个线程先执行? 答:apple()先执行。此时两个fruit对象的class类模板只有一个,class锁是唯一的,所以多个对象使用的仍然是同一个class锁。
fruit.apple();
fruit.banana();
public static synchronized void apple(){}
public synchronized void banana(){}
问题7.一个fruit对象调用方法,apple()是静态同步方法,banana()是普通同步方法,哪一个线程先执行? 答:banana()先执行。apple()方法的锁,锁的是class。banana()方法的锁,锁的是fruit对象。两个方法用的不是同一把锁。
fruit1.apple();
fruit2.banana();
public static synchronized void apple(){}
public synchronized void banana(){}
问题8.两个fruit对象调用方法,apple()是静态同步方法,banana()是普通同步方法,哪一个线程先执行? 答:banana()先执行。apple()方法的锁,锁的是class。banana()方法的锁,锁的是fruit对象。两个方法用的不是同一把锁。
四*、集合类不安全
List在单线程的情况下是安全的,但是多线程呢?
public class demo4 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
解决方案详见 JUC—集合的线程安全吗?
五、Callable(简单)
Callable接口类似于Runnable,因为他们都是为其实例可能由另一个线程执行的类设计的。然而Runnable不返回结果,也不能抛出被检查的异常。
Callable可以有返回值,可以抛出异常,与Runnable方法不同(call方法)
public class demo5 {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask futureTask = new FutureTask(callable);
new Thread(futureTask).start();
try {
String result = (String)futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
return "jess";
}
}
六*、常用辅助类
1. CountDownLatch
使一个线程等待其他线程各自执行完毕后再执行。
CountDownLatch是通过一个计数器来实现的(减法计数器),计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
public class demo6 {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "执行完毕...");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("全部执行完毕!!!");
}
}
2. CyclicBarrier
利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。
public class demo7 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("线程执行成功!!!");
});
for (int i = 0; i < 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 执行 "+temp);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
若计数器等待的计数值变成8,而线程数量仍然是7不变,那 “线程执行成功!!!” 将不会打印,因为计数器只有7。
3*. Semaphore
Semaphore(信号量):其内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,则表示没有共享资源可以访问,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
public class demo8 {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"得到共享资源");
semaphore.release();
System.out.println(Thread.currentThread().getName()+"释放共享资源");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
下篇:juc–并发编程的核心问题总结②
文章思路来自学习视频
|