文章目录
并发编程三要素
- 原子性:和数据库事务特性的原子性类似,一组操作要么全部失败,要么全部成功
- 可见性:某个线程对变量的修改,对其它线程是可见的
- 有序性:java编译器为了提升效率,会改变代码顺序,这违背了有序性,通过happen-before先行发送原则可以保证有序性
并发编程内存模型
多线程
多线程的目的是为了提高cpu的利用率,我们在学设计模式的时候单例模式的完美方案是要考虑多线程的,在学多线程我们需要了解操作系统讲的进程和线程、死锁,了解jvm的知识
创建线程的三种方式
- 继承Thread,一般不推荐,因为java是单继承的
- 实现Runnable接口,重写run方法
- 实现Callable接口,Callable 可以有返回值,返回值通过 FutureTask 进行封装,
volatile
参考: https://jenkov.com/tutorials/java-concurrency/volatile.html
- 不满足原子性
- 满足可见性
- 可以通过内存屏障防止指令重排保证有序性
场景
参考: https://blog.csdn.net/xichenguan/article/details/119425408
-
当一个变量依赖其他变量或变量的新值依赖旧值时,不能用volatile
-
适用场合:多个线程读,一个线程写的场合
-
使用场景:通常被 作为标识完成、中断、状态的标记,值变化应具有原子性
这里简单说一说状态标志:比如有个boolean变量,如果多个线程都要访问它,作为判断标志,这时就可以使用volatile修饰该变量,可以保证多线程对变量的可见性
synchronized
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock
思考:volatile怎么保证可见性、volatile怎么保证有序性
四个窗口卖500张票:加sychronized
package interview.sort.test;
public class SellTicket implements Runnable {
int ticket = 500;
public void sellTicket() throws InterruptedException {
synchronized (this) {
while (ticket > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket-- + "张票");
}
}
}
@Override
public void run() {
try {
sellTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
Thread t1 = new Thread(sellTicket);
Thread t2 = new Thread(sellTicket);
Thread t3 = new Thread(sellTicket);
Thread t4 = new Thread(sellTicket);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t4.setName("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
四个窗口卖500张票:加可重入锁ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class SellTickets implements Runnable {
int ticket = 500;
private Lock lock = new ReentrantLock();
public void sellTicket() throws InterruptedException {
lock.lock();
while (ticket > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket-- + "张票");
}
lock.unlock();
}
@Override
public void run() {
try {
sellTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test {
public static void main(String[] args) {
SellTickets sellTicket = new SellTickets();
Thread t1 = new Thread(sellTicket);
Thread t2 = new Thread(sellTicket);
Thread t3 = new Thread(sellTicket);
Thread t4 = new Thread(sellTicket);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t4.setName("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
四个窗口卖500张票:使用AtomicInteger实现用的是底层是CAS算法
import java.util.concurrent.atomic.AtomicInteger;
public class SellTicket implements Runnable {
AtomicInteger ticket = new AtomicInteger(500);
public void sellTicket() throws InterruptedException {
while (ticket.get() > 0) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket.decrementAndGet() + "张票");
}
}
@Override
public void run() {
try {
sellTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
Thread t1 = new Thread(sellTicket);
Thread t2 = new Thread(sellTicket);
Thread t3 = new Thread(sellTicket);
Thread t4 = new Thread(sellTicket);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t4.setName("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
区别
- 实现:synchronized是jvm实现的,ReentrantLock是jdk实现的
- 性能:sychronized做了优化,速度和ReentrantLock差不多,前者的锁,jvm会帮我们自动释放,后者需要手动释放锁
- 公平:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,synchronized是公平锁,ReetrantLock是非公平锁
- 中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,ReentrantLock 可中断,而 synchronized 不行
- 绑定多对象:ReentrantLock可以绑定多个Condition对象
线程池
建立线程池好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消- 耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
常见线程池
- CachedThreadPool:一个任务创建一个线程;
- FixedThreadPool:所有任务只能使用固定大小的线程;
- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool;
- newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池
- newWorkSteadingThreadPool: 创建一个抢占式执行的线程池(任务执行顺序不确定),此方法是 JDK 1.8 版本新增的
ExecutorService executorService1 = Executors.newScheduledThreadPool(12);
ThreadPoolExecutor是Executor的实现类,可以通过它的execute()方法创建线程,构造方法里面需要设置很多参数
ThreadPoolExcutor
- corePoolSize: 核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间,当线程数量大于核心线程数小于最大线程数时,存在空闲的线程它们的存活时间就是这个,当存活的线程数小于等于核心线程数就失效了
- unit:空闲线程的存活时间的单位
- workQueue:工作队列,当线程数大于最大线程数时会将线程放到工作队列中,这里用的是阻塞队列BlockingQueue
- threadFactory:线程工厂,用于创建工作线程
- handler: 当工作队列达到上限的时候采取拒绝策略,常见的拒绝策略就是抛出异常
阻塞队列
-
ArrayBlockingQueue:先进先出,阻塞队列,插入元素时队列满了会一直等待。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
-
SynchronousQueue:数组结构的阻塞队列。队列满了以后,任何一次插入操作的元素都要等待相对的删除/读取操作。
-
LinkedBlockingQueue:先进先出,链表结构非阻塞队列,适合插入和删除较多的操作。
-
DelayedWorkQueue:延迟队列,延迟的时间还未到的获取的任务会返回空。
-
PriorityBlockingQueue:优先级队列,元素必须实现Comparable接口,优先级最高的始终在队列的头部,任务会优先执行。
线程中断
- 在捕获异常中抛出中断异常
- 将interrupted()方法当作标志状态,做判断,没中断在执行后面的语句
- Executor可以通过shutdown()中断所有线程,如果要中断某一个线程就是用submit()方法会返回一个Future对象,该对象调用cancel(true)方法中断线程
基础线程机制
-
线程睡眠sleep():单位是毫秒,是个本地方法,执行此方法会让线程睡眠
-
yield():是个本地方法,执行此方法代表当前线程已经执行差不都了,可以切换另外的线程;该方法只是向线程调度器提供一个建议,建议具有相同优先级的其它线程可以运行
-
守护线程setDaemon():这时final修饰的方法,执行此方法是将线程设置为守护线程(后台线程),守护线程是指在后台执行的一种服务线程,这种线程不是必须的,当所有的非守护线程执行完毕后,程序停止,会杀死所有的守护线程
-
线程优先级setPriority():final修饰的方法,为线程设置优先级,可以是1-10级,10级最高,目的是为了告诉线程调度器优先执行优先级高的线程
线程之间的协作
join(): final()修饰的方法,执行此方法当前线程将会挂起,执行目标线程后,在执行当前线程
wait()、notify()、notifyAll()
- 调用wait()方法会使当前线程挂起,会释放锁,其它线程执行满足这个条件时其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程
- final修饰,是本地方法,并且是Object里面的方法
- 只能在同步方法或者同步控制块中使用
sleep()和wait()的区别
- sleep()是Thread类的方法,wait()是Object的方法
- 执行wait()时会释放锁,sleep()则不会释放锁
- wait()只能在synchronized中使用
await()、signal()、signalAll()
- juc包下Condition的方法,一个Lock可以创建多个Condition,synchronized块搭配wait()、notify()、notifyAll(),相当于仅有一个Condition,所有线程的调度通信都是由这个Condition完成的,不够灵活
- await()对用wait(),signal()对用notify(),signalAll()对用notifyAll()
线程的状态
参考:https://www.cnblogs.com/IUbanana/p/7110297.html
-
新建状态(New):新线程对象已经创建,还没有在其上调用start()方法
-
就绪状态(Runnable):当前线程调用了start()方法,随时等待CPU调度执行
-
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞 – 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态
-
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,当前线程的任务已处理完毕,释放CPU资源,运行结束的状态;
线程的状态这个人讲的不错:https://blog.csdn.net/weixin_44673534/article/details/121344594