再战JUC
整合的狂神和尚硅谷的,感觉尚硅谷讲的有点乱但很深刻,狂神的很浅但是调理清晰两者结合起来很好
进程和线程
进程是计算机中的程序关于某数据集合上的一次运行活动是系统进行资源分配和调度的基本单位是操作系统结构的基础
线程是操作系统能够进行调度的最小单位
并发和并行
并发
并发的关键是你有处理多个任务的能力,不一定要同时,它们利用操作系统的CPU时间分片功能,其中每个任务运行其任务的一部分,然后进入等待状态。当第一个任务处于等待状态时,会将CPU分配给第二个任务以完成其一部分任务。
对于单核(一个CPU)来说,通过快速切换来模拟一种同时完成的假象
并行
并行的关键是你有同时处理多个任务的能力
对于多核来说,多个线程可以同时进行
并发编程的实质:充分利用CPU资源
普通方法和多线程
线程的创建与运行
-
继承Thread,重写run方法,主线程start开启副线程
//创建线程方式一:继承Thread,重写run()方法,调用start开启线程
public class TestThread extends Thread {
-
实现Runnable接口,实现run方法,然后在主线程中创建Runnable实现类对象然后调用
new Thread().start()
public class TestRunnable implements Runnable{
-
实现Callable接口
为什么实现Runnable接口后要使用FutureTask对象因为Thread不包含实现Callable接口类的构造方法
所以为了能够实现Thread类所以我们要调用一个桥接Callable接口和Runnable接口的类这就是FutureTask
FutureTask提供了Callable的构造方法并且实现了Runnable接口
-
实现Callable接口,重写call()方法
-
创建构造方法包含Callable实现类对象
class MyThread implements Callable {
线程的五大状态
线程停止
最好是让线程自己停止下来通过外部标志位来停止线程
public class StopThread implements Runnable{
private static boolean flag = true;
线程休眠
-
sleep(时间)指当前线程阻塞的毫秒数
-
sleep存在异常
-
sleep时间达到后线程进入就绪状态
-
sleep可以模拟网络延时,倒计时等
-
每一个对象都有一把锁,sleep不会释放锁
线程礼让
-
礼让线程,让当前正在执行的线程暂停但不阻塞
-
将线程从运行状态转为就绪状态
-
让CPU重新调度,礼让不一定成功
class MyYield implements Runnable {
线程强制执行
线程强制执行Join
public class TestJoin implements Runnable{
观察线程状态
线程优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来访问
-
线程的优先级用数字表示,范围从1——10 Thread.MIN_PRIORITY = 1; Thread.MAX_PRIORITY = 10; Thread.NORM_PRIORITY = 5;
-
使用以下方式改变或后去优先级 getPriority().setPriority(int xxx)
class MyPriority implements Runnable {
守护线程
-
线程分为用户线程和守护线程
-
虚拟机必须确保用户线程执行完毕
-
虚拟机不用等待守护线程执行完毕
-
如:后台的监控日志,操作日志,垃圾回收....
public class TestDaemon {
public static void main(String[] args) {
You you = new You();
God god = new God();
Thread thread = new Thread(god);
//默认为false为用户
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
class You implements Runnable {
线程同步
因为并发是指具有处理多个任务的能力,应用到业务场景中也就是同一个对象可以被多个线程同时操作
处理多线程问题时,多个线程访问同一个对象,而且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的进行进入这对象的等待池形成队列,等待前面线程使用完毕下一个线程再使用
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了冲突问题,为了保证数据在方法中被访问的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其它线程必须等待后释放锁即可,存在以下问题
-
一个线程持有锁会导致其他所有需要此锁的线程挂起
-
在多线程竞争下,加索,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引发性能问题
ArrayList线程不安全问题
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 40; i++) {
new Thread(() -> {
//向集合中添加元素
list.add(UUID.randomUUID().toString().substring(0, 8));
//获取集合元素
System.out.println(list);
}, String.valueOf(i)).start();
}
}
原因是add方法没有添加synchronized在多线程条件下容易起冲突
解决办法
-
Vector
-
Collections.synchronizedList(new ArrayList<>())
-
CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
ConcurrentModificationException并发修改异常
HashSet线程不安全问题
这里多线程下add方法引发的问题就不做过多解释了重点了解解决办法CopyOnWriteArraySet
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
//向集合中添加内容
set.add(UUID.randomUUID().toString().substring(0, 8));
//展示set集合
System.out.println(set);
}, String.valueOf(i)).start();
}
}
HashMap线程不安全问题
解决办法ConcurrentHashMap
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(() -> {
//向集合中添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
//展示set集合
System.out.println(map);
}, key).start();
}
}
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源。某一个代码块同时拥有两个以上对象的锁时才可能发生死锁现象
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "持有a并且试图获取b");
try {
TimeUnit.SECONDS.sleep(1); // 确保持有a
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("获取b");
}
}
},"AA").start();
new Thread(() -> {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "持有b并且试图获取a");
synchronized (a) {
System.out.println("获取a");
}
}
},"BB").start();
}
Lock锁
-
通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
-
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
-
ReentrantLock(可重入锁)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可显示加锁、释放锁
class LTicket {
//票的数量
private int number = 35;
//创建可重入锁(公平锁)
private final ReentrantLock lock = new ReentrantLock(true);
//卖票方法
public void sale() {
//上锁
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + ":卖出:" + (number--) + "剩余:" + number);
}
} finally {
//解锁
lock.unlock();
}
}
}
public class LSaleTicket {
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();
}
}
}, "第一个售票员").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
}, "第二个售票员").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
}, "第三个售票员").start();
}
}
Synchronized和Lock的区别
-
Lock是显示锁(手动开启和关闭锁、别忘记关闭锁)synchronized是隐式锁,除了作用域自动释放
-
Lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性
-
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
线程通信(这就是继承Thread的好处)
生产者消费者模式
-
假设仓库中只能存放一件产品,生产者将生产出来的产品放到仓库,消费者将仓库中产品取走消费
-
如果仓库中没有产品,则生产者将产品放到仓库,否则停止生成并且等待,直到仓库中的产品被消费者取走为止
-
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
这就是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件
管程法——用一个缓冲区模拟
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Productor(synContainer).start();
new Consumer(synContainer).start();
}
}
//这就是继承Thread的好处:可以通过构造方法进行传参
class Productor extends Thread {
SynContainer synContainer;
public Productor(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synContainer.push(new Chicken(i));
System.out.println("生成了第" + i + "只鸡");
}
}
}
class Consumer extends Thread {
SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了第" + synContainer.pop().getId() + "只鸡");
}
}
}
//产品
class Chicken {
private int id;
public Chicken(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
//缓冲区
class SynContainer {
//缓冲区大小
Chicken[] chickens = new Chicken[10];
int count = 0;
//将产品放入缓冲区中
public synchronized void push(Chicken chicken) {
//如果容器满了,就需要消费者消费
while (count == chickens.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满我们就丢入产品
chickens[count] = chicken;
count++;
//通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop() {
while (count == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
Chicken chicken = chickens[count];
this.notifyAll();
return chicken;
}
}
信号灯法
线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程、放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
好处:
-
提高响应速度(减少了创建新线程的时间)
-
降低资源消耗(重复利用线程池中线程,不需每次都创建)
-
便于线程管理
public class TestPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.shutdown();
}
}
class MyThread implements Runnable {