JUC入门(一)
进程与线程的区别
进程:系统正在运行的一个应用程序,程序一旦运行就是进程,进程是资源分配的最小单位
线程:线程是程序执行的最小单位
线程的基本状态
1)new 新建
2)runnable 准备就绪
3)blocked 阻塞
4)waiting 不见不散的等待
5)timed_waiting 过时不候
6)terminated 终结
wait和sleep的区别
1)sleep是Thread的静态方法,wait是Object的方法,任何对象都能够调用
2)sleep不会释放锁,也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁
3)他们都可以被interrupted中断
并发和并行
并发:同一时刻多个线程在访问同一个资源
并行:多项工作一起同时执行
管程 (Monitor)
是一种同步机制,保证同一时间内,只有一个线程访问被保护数据或者代码
jvm的同步是基于进入和退出的时候,使用管程对象实现的
用户线程和守护线程
用户线程:自定义线程,如果用户线程还存活,jvm就不会关闭
public static void main(String[] args){ Thread aa = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon()); while(true){ } }, "aa"); aa.start(); }
可以看到下图中一直在死循环,没有return 0;
守护线程:比如垃圾回收,如果没有用户线程在执行了,即使守护线程还在,jvm也会结束
public static void main(String[] args){ Thread aa = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon()); while(true){ } }, "aa"); aa.setDaemon(true); //改为守护线程 aa.start(); }
可以看到,即使守护线程运行还没结束,主程序已经return
Synchronized
修饰的对象有以下几种
1)修饰一个代码块,被修饰的代码块称为同步代码快
2)修饰一个方法,被修饰的方法称为同步方法
3)修饰一个静态的方法,作用的范围是整个静态方法,作用的对象是这个类的所有对象
4)修饰一个类,作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
一个关于Synchronized的一个买票小例子
class Ticket{ private int number=30; public synchronized void sale(){ if(number>0){ System.out.println(Thread.currentThread().getName()+"卖出"+(number--)+"剩下"+number); } } } public class Sale { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(new Runnable() { @Override public void run() { for(int i=0;i<40;i++){ ticket.sale(); } } },"AA").start(); new Thread(new Runnable() { @Override public void run() { for(int i=0;i<40;i++){ ticket.sale(); } } },"BB").start(); new Thread(new Runnable() { @Override public void run() { for(int i=0;i<40;i++){ ticket.sale(); } } },"CC").start(); } }
Lock
除了synchronized锁,在JDK 1.5,Java提供了更强大的线程同步机制,比起 synchronized 隐式的方式,虽然可以看到它锁的代码片段,但看不到更具体的;不同synchronized锁,Lock锁可以看到它的开始和结束。(用法也是如此,手动创建Lock锁,需要手动关闭Lock锁)
Lock是显性的方式来实现同步
Lock与Synchronized的区别
1)Lock不是java语言内置的,synchronized是java的关键字,因此是内置特性,Lock是类,通过这个类可以实现同步访问
2)Lock和Synchronized有一点非常不同,采用synchronized不需要用户手动加锁解锁,当synchronized方法或者synchronized代码块执行完或者发生异常之后,系统会自动的让线程释放对锁的占用,而Lock则必须手动去释放锁,如果没有释放,则会造成死锁现象
3)Lock可以让等待的线程响应中断,而synchronized却不行,使用synchronized时,线程会一直执行下去,不能够响应中断
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
5)Lock可以提高多个线程进行读操作的效率
使用lock锁来实现买票问题
class LTicket{ private int number=30; private final ReentrantLock lock = new ReentrantLock(); public void sale(){ try{ lock.lock(); if(number>0){ System.out.println(Thread.currentThread().getName()+"卖出"+(number--)+"剩下"+number); } }finally { lock.unlock(); } } } public class SaleByLock { public static void main(String[] args) { LTicket ticket = new LTicket(); new Thread(new Runnable() { @Override public void run() { for(int i=0;i<40;i++){ ticket.sale(); } } },"AA").start(); new Thread(new Runnable() { @Override public void run() { for(int i=0;i<40;i++){ ticket.sale(); } } },"BB").start(); new Thread(new Runnable() { @Override public void run() { for(int i=0;i<40;i++){ ticket.sale(); } } },"CC").start(); } }
使用Synchronized关键字 一个线程通信的例子(线程A当number==0时执行++,线程B当number==1时,执行--,要实现,一个加一个减 一个加一个减)
class share{ private int number=0; public synchronized void incr() throws InterruptedException { if(number!=0){ this.wait(); } number++; System.out.println(Thread.currentThread().getName()+":number"+number); notifyAll(); } public synchronized void desc() throws InterruptedException { if(number==0){ this.wait(); } number--; System.out.println(Thread.currentThread().getName()+":number"+number); notifyAll(); } } public class XCTX { public static void main(String[] args){ share share = new share(); new Thread(()->{ for(int i=1;i<=10;i++){ try { share.incr(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(int i=1;i<=10;i++){ try { share.desc(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); } }
执行结果如下:
线程通信的虚假唤醒问题
在上面所述的例子中,如果我们多增加两个线程C、D,一个执行++,一个执行--,那么,就有可能会出现以下问题(虚假唤醒)
那么,为什么会出现这样的问题呢?原因是出现在if语句
我们有4个线程A、B、C、D、两个加两个减
当我们执行B线程(进行减操作)结束的时候,就会唤醒其他线程,其他线程会根据值来判定是否wait
其中A跟C是满足条件继续执行的,但是因为加了synchronized,所以只有一个能执行
这里我们假设A拿到了锁,成功执行,那么C就只能进入wait
当A结束的时候,就会唤醒其他线程
而这个时候,因为C之前已经进入过了wait,而这是一个if语句(也就是不会再次判定),那么C就能够直接往下继续执行
但是C在这个时候,我们是希望等待的。
这就是虚假唤醒
如何解决虚假唤醒?
只需要将if语句改成while语句,进行循环判断即可
class share{ private int number=0; public synchronized void incr() throws InterruptedException { while(number!=0){ this.wait(); } number++; System.out.println(Thread.currentThread().getName()+":number"+number); notifyAll(); } public synchronized void desc() throws InterruptedException { while(number==0){ this.wait(); } number--; System.out.println(Thread.currentThread().getName()+":number"+number); notifyAll(); } } public class XCTX { public static void main(String[] args){ share share = new share(); new Thread(()->{ for(int i=1;i<=10;i++){ try { share.incr(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(int i=1;i<=10;i++){ try { share.desc(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); new Thread(()->{ for(int i=1;i<=10;i++){ try { share.incr(); } catch (InterruptedException e) { e.printStackTrace(); } } },"C").start(); new Thread(()->{ for(int i=1;i<=10;i++){ try { share.desc(); } catch (InterruptedException e) { e.printStackTrace(); } } },"D").start(); } }
使用lock锁实现线程通信
class share2{ private int number=0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void incr(){ lock.lock(); try{ while(number!=0){ condition.await(); } number++; System.out.println(Thread.currentThread().getName()+":number"+number); condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void decr(){ lock.lock(); try{ while(number!=1){ condition.await(); } number--; System.out.println(Thread.currentThread().getName()+":number"+number); condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class XCTX2 { public static void main(String[] args) { share2 share = new share2(); new Thread(()->{ for(int i=1;i<=10;i++){ share.incr(); } },"A").start(); new Thread(()->{ for(int i=1;i<=10;i++){ share.decr(); } },"B").start(); } }
实现一个定制化通信(依次打印“A线程打印5次A,B线程打印10次B,C线程打印15次C”10次)
思路:
condition是用来干嘛的? 是多线程间协调通信的工具类
1)创建一个ReentrantLock()对象,并且创建3个用来通信的condition,c1 c2 c3
2)实现三个print方法,分别对应题目要求,每一个方法里用flag来维护是否等待,唤醒操作用condition.signal();
3)使用3个线程取调用这三个方法,循环10次
package com.ma; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class ShareResource{ private int flag=1; private Lock lock = new ReentrantLock(); private Condition c1 = lock.newCondition(); private Condition c2 = lock.newCondition(); private Condition c3 = lock.newCondition(); public void print15() throws InterruptedException { lock.lock(); try{ while(flag!=3){ c3.await(); } for(int i=1;i<=15;i++){ System.out.println(Thread.currentThread().getName()+":C"); } flag=1; c1.signal(); }finally { lock.unlock(); } } public void print10() throws InterruptedException { lock.lock(); try{ while(flag!=2){ c2.await(); } for(int i=1;i<=10;i++){ System.out.println(Thread.currentThread().getName()+":B"); } flag=3; c3.signal(); }finally { lock.unlock(); } } public void print5() throws InterruptedException { lock.lock(); try{ while(flag!=1){ c1.await(); } for(int i=1;i<=5;i++){ System.out.println(Thread.currentThread().getName()+":A"); } flag=2; c2.signal(); }finally { lock.unlock(); } } } public class XCTX3 { public static void main(String[] args){ ShareResource shareResource = new ShareResource(); new Thread(()->{ try { for(int i=1;i<=10;i++) { shareResource.print5(); } } catch (InterruptedException e) { e.printStackTrace(); } },"A").start(); new Thread(()->{ try { for(int i=1;i<=10;i++) { shareResource.print10(); } } catch (InterruptedException e) { e.printStackTrace(); } },"B").start(); new Thread(()->{ try { for(int i=1;i<=10;i++) { shareResource.print15(); } } catch (InterruptedException e) { e.printStackTrace(); } },"C").start(); } }
集合的线程安全问题
集合线程不安全的一个例子
public class CollectionSafe { public static void main(String[] args){ List<String> list = new ArrayList<>(); for(int i=1;i<=30;i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); },String.valueOf(i)).start(); } } }
执行上述代码的时候,有很大概率会触发并发修改异常
如何解决?
1)因为ArrayList是不安全的,所以可以用vector(线程安全)来替换
List<String> list = new Vector<>();
2)使用Collections解决
List<String> list = Collections.synchronizedList(new ArrayList<>());
3)使用CopyOnwriteArrayList
List<String> list = new CopyOnwriteArrayList<>();
那么CopyOnwriteArrayList为什么能够避免并发修改问题呢?
1)CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet。
2)很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
3)优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了
4)缺点也很明显,一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。
HashMap的并发修改异常
例子如下
Map<String,String> map = new HashMap<>(); for(int i=1;i<=30;i++){ String key=String.valueOf(i); new Thread(()->{ map.put(key,UUID.randomUUID().toString().substring(0,8)); System.out.println(map); }).start(); }
我们可以用ConcurrentHashMap<>()来避免这个问题
Map<String,String> map = new ConcurrentHashMap<>(); for(int i=1;i<=30;i++){ String key=String.valueOf(i); new Thread(()->{ map.put(key,UUID.randomUUID().toString().substring(0,8)); System.out.println(map); }).start(); }