JUC并发编程
1、什么是JUC
JUC即java.util.concurrent
**涉及三个包: **
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
2、线程和进程
2.1 什么是线程和进程
**进程: **一个程序。如QQ、wechat等
**线程: **程序中的 某些操作。如打开了Typora,输入、保存等就是线程。
一个进程至少包含一个线程!
java中默认有两个线程,一个是main线程,另外一个是GC线程。
**java能自己开启线程吗? **
答:不能,java是通过调用本地c++方法开启的线程。如下图所示:
2.2 并发与并行的区别
**并发: **单核CPU模拟出多条线程,不断交替,为快不破。
**并行: **多核CPU,多线程同时执行。
如何查看CPU核数?
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
}
2.3 线程的六种状态
Thread中有个内部枚举类Thread.State其中定义了线程的六种状态,源码如下:
/** 新生
* Thread state for a thread which has not yet started.
*/
NEW,
/** 运行
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/** 阻塞
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/** 一直等待,等到地老天荒
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/** 有时间的等待
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/** 线程终止
* Thread state for a terminated thread.
* The thread has completed execution.
*/
2.4 wait与sleep的区别
-
来着不同的类
wait =>Object,sleep=>Thread
-
关于锁得释放
wait =>释放锁,sleep=>占用锁,抱着锁睡
-
使用范围
wait =>同步代码块中使用,sleep=>想在哪睡在哪睡,任何地方使用
3、Lock锁
3.1 什么是Lock锁
如下图:
3.2 如何使用
Lock lock = new ReentrantLock();
//加锁
lock.lock();
try{
//业务代码
}finally {
//解锁
lock.unlock();
}
3.3 Lock与Synchronize的区别
-
来源不同
synchronize => java的内置关键字,在jvm层;Lock =>java的一个接口
-
获取锁得方式不同
synchronize => 自动获取锁,不能判断锁得状态;Lock => 手动获取锁,可判断是否获取到锁
-
线程阻塞方面
synchronize => 线程1阻塞会导致线程2永远等待;Lock=>不一定会等下去
-
锁得类型不同
synchronize=>可重入锁、不可中断、非公平;Lock=>可重入锁、可判断锁、非公平(可设置成公平)
-
使用范围不同
synchronize=>适用于少量代码块同步;Lock=>适合锁大量的同步代码块
3.4 synchronize与lock模拟多线程抢票
synchronize
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
},"A").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
},"B").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
},"C").start();
}
}
class Ticket{
private static int ticketAmount = 100;
public synchronized void saleTicket(){
if(ticketAmount>0){
System.out.println(Thread.currentThread().getName()+"购买了第"+ticketAmount--+"张票");
}
}
}
Lock
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
},"A").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
},"B").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
},"C").start();
}
}
class Ticket{
private static int ticketAmount = 100;
public void saleTicket(){
//创建lock锁
Lock lock = new ReentrantLock();
//加锁
lock.lock();
try {//业务代码
if(ticketAmount>0){
System.out.println(Thread.currentThread().getName()+"购买了第"+ticketAmount--+"张票");
}
}finally {
//解锁
lock.unlock();
}
}
}
4、生产者和消费者模式
线程之间的通信问题就是生产者和消费者问题,即线程的交替执行。比如两个线程同时操作一个变量,就涉及到通知、等待和唤醒。其实线程间的通信主要就是通知、业务执行和唤醒。
4.1 synchronize版的生产者和消费者模式
public class Test {
public static void main(String[] args) {
PC pc = new PC();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.add();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.decrease();
}
},"B").start();
}
}
class PC{
private static int amount=0;
public synchronized void add(){
if (amount>0){//如果大于0,不"生产"(加1),线程进入等待状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount++;
System.out.println(Thread.currentThread().getName()+"执行了加法,amount="+amount);
//唤醒其他线程
notifyAll();
}
public synchronized void decrease(){
if(amount == 0){//如果amount=0,不消费(不减1),线程进入等待状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount--;
System.out.println(Thread.currentThread().getName()+"执行了减法,amount="+amount);
//唤醒其他线程
notifyAll();
}
}
执行结果如下图:
4.2 synchronize模拟生产者和消费者模式产生的问题和解决方法
同样是4.1中的代码如果同时开启4个(超过2个线程)代码如下
public class Test {
public static void main(String[] args) {
PC pc = new PC();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.add();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.decrease();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.add();
}
},"C").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.decrease();
}
},"D").start();
}
}
class PC{
private static int amount=0;
public synchronized void add(){
if (amount>0){//如果大于0,不"生产"(加1),线程进入等待状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount++;
System.out.println(Thread.currentThread().getName()+"执行了加法,amount="+amount);
//唤醒其他线程
notifyAll();
}
public synchronized void decrease(){
if(amount == 0){//如果amount=0,不消费(不减1),线程进入等待状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount--;
System.out.println(Thread.currentThread().getName()+"执行了减法,amount="+amount);
//唤醒其他线程
notifyAll();
}
}
执行结果如下图:
上图中的执行出问题了,得到的结果并不是0,1了。出现了意料之外的结果。为什么会出现这样的结果?
答:从执行结果中我们发现感觉是线程B等待失效。然后我们去官方文档中找到Object类中的wait方法,我们看到这样一段话,如下图:
按照jdk帮助文档的提示,造成错误的原因是wait是不占用锁的,在多个线程同时被唤醒的时候有一个随机抢锁的过程,导致了不该唤醒的线程抢到了锁被唤醒了,即虚假唤醒。解决方法也很简单,就是将判断方法中的if改成while即可。while与if的不同在于,当等待的线程被唤醒后,if判断的线程会直接往下走,而while判断的线程需要重新判断才能往下走。所以jdk官方文档推荐使用while代替if从而解决虚假唤醒的现象。
4.3 Lock锁模拟生产者和消费者模式
使用Condition工具类去作为Object监视器,用signal()和await()方法代替synchronized中的notify()和wait()
Condition的优势在于能精准的唤醒线程
public class Test02 {
public static void main(String[] args) {
PC2 pc = new PC2();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.add();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.decrease();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.add();
}
},"C").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.decrease();
}
},"D").start();
}
}
class PC2{
private static int amount=0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void add(){
lock.lock();
try{
while(amount>0){//如果大于0,不"生产"(加1),线程进入等待状态
try {
//等待
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount++;
System.out.println(Thread.currentThread().getName()+"执行了加法,amount="+amount);
//唤醒其他线程
condition.signalAll();
}finally {
lock.unlock();
}
}
public void decrease(){
lock.lock();
try{
while(amount == 0){//如果amount=0,不消费(不减1),线程进入等待状态
try {
//等待
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount--;
System.out.println(Thread.currentThread().getName()+"执行了减法,amount="+amount);
//唤醒其他线程
condition.signalAll();
}finally {
lock.unlock();
}
}
}
执行结果如下图:
4.4 Lock锁实现精确唤醒
实现目标:A、B、C、D四个线程顺序执行。
public class Test02 {
public static void main(String[] args) {
PC2 pc = new PC2();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.add();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.decrease();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.add();
}
},"C").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
pc.decrease();
}
},"D").start();
}
}
class PC2{
private static int amount=0;
Lock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
Condition conditionD = lock.newCondition();
public void add(){
lock.lock();
try{
while (amount>0){//如果大于0,不"生产"(加1),线程进入等待状态
try {
//等待
if("A".equals(Thread.currentThread().getName())){
conditionA.await();
}else if("C".equals(Thread.currentThread().getName())){
conditionC.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount++;
System.out.println(Thread.currentThread().getName()+"执行了加法,amount="+amount);
//唤醒其他线程
if("A".equals(Thread.currentThread().getName())){
conditionB.signal();
}else if("C".equals(Thread.currentThread().getName())){
conditionD.signal();
}
}finally {
lock.unlock();
}
}
public void decrease(){
lock.lock();
try{
while(amount == 0){//如果amount=0,不消费(不减1),线程进入等待状态
try {
if("B".equals(Thread.currentThread().getName())){
conditionB.await();
}else if("D".equals(Thread.currentThread().getName())){
conditionD.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
amount--;
System.out.println(Thread.currentThread().getName()+"执行了减法,amount="+amount);
//唤醒其他线程
if("B".equals(Thread.currentThread().getName())){
conditionC.signal();
}else if("D".equals(Thread.currentThread().getName())){
conditionA.signal();
}
}finally {
lock.unlock();
}
}
}
执行结果:
5、八个关于锁的问题
5.1 先发短信还是先打电话
public class One {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread( () -> {
phone.call();
},"A").start();
new Thread( () -> {
phone.sendMsg();
},"B").start();
}
}
class Phone{
public synchronized void call(){
System.out.println("打电话");
}
public synchronized void sendMsg(){
System.out.println("发短信");
}
}
先打电话,再发短信
5.2 延迟后是先发短信还是先打电话
public class Two {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread( () -> {
phone.call();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread( () -> {
phone.sendMsg();
},"B").start();
}
}
class Phone2{
public synchronized void call(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public synchronized void sendMsg(){
System.out.println("发短信");
}
}
还是先打电话再发短信
5.3 两个对象调用不同的方法,执行的先后顺序
public class Three {
public static void main(String[] args) {
Phone3 phoneA = new Phone3();
Phone3 phoneB = new Phone3();
new Thread( () -> {
phoneA.call();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread( () -> {
phoneB.sendMsg();
},"B").start();
}
}
class Phone3{
public synchronized void call(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public synchronized void sendMsg(){
System.out.println("发短信");
}
}
先发短信后打电话
5.4 一个同步方法一个普通方法,谁先执行
public class Four {
public static void main(String[] args) {
Phone4 phone = new Phone4();
new Thread( () -> {
phone.call();
},"A").start();
new Thread( () -> {
phone.sendMsg();
},"B").start();
}
}
class Phone4{
public synchronized void call(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public void sendMsg(){
System.out.println("发短信");
}
}
先执行普通方法发短信,再执行打电话
5.5 同步方法都是静态方法,同一个对象,先执行哪个
public class Five {
public static void main(String[] args) {
Phone5 phone = new Phone5();
new Thread( () -> {
phone.call();
},"A").start();
new Thread( () -> {
phone.sendMsg();
},"B").start();
}
}
class Phone5{
public static synchronized void call(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public static synchronized void sendMsg(){
System.out.println("发短信");
}
}
先执行打电话,后执行发短信。
5.6 两个对象调用不同的静态同步方法,谁先执行
public class Six {
public static void main(String[] args) {
Phone6 phoneA = new Phone6();
Phone6 phoneB = new Phone6();
new Thread( () -> {
phoneA.call();
},"A").start();
new Thread( () -> {
phoneB.sendMsg();
},"B").start();
}
}
class Phone6{
public static synchronized void call(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public static synchronized void sendMsg(){
System.out.println("发短信");
}
}
先执行打电话,然后执行发短信
5.7 一个静态同步方法,一个普通同步方法,只有一个对象,先执行哪个
public class Seven {
public static void main(String[] args) {
Phone7 phone = new Phone7();
new Thread( () -> {
phone.call();
},"A").start();
new Thread( () -> {
phone.sendMsg();
},"B").start();
}
}
class Phone7{
public synchronized void call(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public static synchronized void sendMsg(){
System.out.println("发短信");
}
}
先执行发短信,再执行打电话
5.8 一个静态同步方法,一个普通同步方法,两个对象,先执行哪个方法
public class Eight {
public static void main(String[] args) {
Phone8 phoneA = new Phone8();
Phone8 phoneB = new Phone8();
new Thread( () -> {
phoneA.call();
},"A").start();
new Thread( () -> {
phoneB.sendMsg();
},"B").start();
}
}
class Phone8{
public synchronized void call(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public synchronized void sendMsg(){
System.out.println("发短信");
}
}
先执行发短信,后执行打电话
5.9 总结
锁只会锁两个东西,一个是new 出来的对象,另外一个是唯一的class模板。普通的synchronize修饰的同步方法或同步代码块,锁得是调用该方法的对象。static修饰的同步方法或同步代码块,锁的是class唯一的模板,因此调用该同步方法的线程其对象是否一样,锁都是同一个。
6、集合不安全
6.1 ArrayList线程不安全
-
- 不安全的ArrayList
我们开启多个线程向同一个list中添加,结果如下图所示:
- 2.ArrayList线程不安全的原因
我们进入ArrayList的源码可以看到以下代码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
我们从源码中可以发现,ArrayList的底层是个Object数组。在add方法中,先是多数组长度加1,然后将需要添加的对象加到数组的最后。这样就会存在一个问题,就是多个线程同时对数组长度进行了加1,后面线程的值将覆盖前面线程的值。
- 3.如何解决ArraList线程不安全问题
public class UnSafeArrayList {
public static void main(String[] args) {
//1.将ArrayList 改为 Vector
//List<String> list = new Vector<>();
//2.用Collections工具类,将ArrayList转为线程安全的集合类
//List<String> list = Collections.synchronizedList(new ArrayList<>());
//3.用CopyOnWriteArrayList 代替ArrayList
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100; i++) {
new Thread( () ->{
list.add(Thread.currentThread().getName());
},""+i).start();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
总结:Vector之所以是线程安全的,是因为Vector中的add方法是用synchronize修饰的;Collections.synchronizedList()之所以线程安全,是因为Collections.synchronizedList()得到了一个SynchronizedList对象,SynchronizedList中的add、set、get等方法中都使用了synchronized代码块;CopyOnWriteArrayList 之所以是线程安全的是因为CopyOnWriteArrayList 的原理是写入时复制,即在在执行add方法时,将原数组复制一份(长度+1),然后将需要添加的对象加到复制数组的末尾,最后用复制的数组替代原数组,在add方法中使用的是Lock锁,从而保证线程安全,源码如下:
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();
}
}
综上所述推荐使用CopyOnWriterArrayList代替ArrayList解决ArrayList的线程不安全问题,理由是synchronize等于低效率。
6.2 HashSet线程不安全
- 1.不安全的HashSet
我们开启多个线程向同一个set中添加,结果如下图所示:
- 2.Set线程不安全的原因
HashSet的底层就是HashMap
public HashSet() {
map = new HashMap<>();
}
//hashSet的add方法 本质是hashMap的put,set的值就是map中的key,PRESENT是一个常量,所有set是不可重复的
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
- 3.如何解决HashSet的线程不安全问题
public class UnSafeHashSet {
public static void main(String[] args) {
//Set<String> list = new HashSet();
//1.用Collections工具类将HashSet转为线程安全的集合类SynchronizedSet
//Set<String> list = Collections.synchronizedSet(new HashSet<>());
//2.用CopyOnWriteArraySet代替HashSet
Set<String> list = new CopyOnWriteArraySet();
for (int i = 0; i < 30; i++) {
new Thread( () -> {
list.add(Thread.currentThread().getName());
System.out.println(list);
},""+i).start();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
6.3 HashMap线程不安全
- 1.不安全的HashMap
- 2.如何解决HashMap的线程不安全问题
public class UnSafeHashMap {
public static void main(String[] args) {
//1. map 是这么用的吗? 不是,工作中不用HashMap
//2. 默认等价于什么? new HashMap<>(16,0.75)
//Map<String,String> map = new HashMap<>();
//1.用Collections工具类将HashMap转变成线程安全的SynchronizedMap
//Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
//2.用ConcurrentHashMap 代替HashMap
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
new Thread( () -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
7、Callable
- 1.可以有返回值
- 2.可以抛出异常
- 3.方法不同,run()/call()
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
//Thread中只能传Runnable对象,Callable对象无法传入,怎么解决?
//我们在官方文档中找到Runnable接口的一个实现类——FutureTask,其中有个构造方法可传入Callable对象
FutureTask futureTask = new FutureTask(myThread);
new Thread(futureTask,"A").start();
//多个线程调用只会打印一次call(),结果有缓存
new Thread(futureTask,"B").start();
//获取返回值
//可能会阻塞,因为如果call方法中是一个耗时的业务将导致阻塞
//解决阻塞的办法:1.放到最后,2.异步通信
Integer o = (Integer) futureTask.get();
System.out.println(o);
}
}
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("call()");
return 1024;
}
}