3:多线程内存以及线程安全,线程通信
- 静态变量(
static
)在方法区上,变量的值在堆上 - 静态常量(
static final
)在方法区上,常量的值在堆上 - 实例变量(
属于对象的属性,需要在对象的基础上建立
),变量的值在堆上 - 局部变量在线程对应的栈上,直接赋值的值在栈上,如果是创建的对象赋值则在堆上
线程私有内存:
每一个线程都有自己对应的栈,这些局部变量都属于线程私有的,其它线程是相互隔离的,JVM允许满足某些条件的线程使用。
线程共享内存
:各个线程共享的内存是方法区和堆,至于线程是否能使用方法区和堆上的数据,取决于这些数据的可见性。
线程安全:
如果多线程环境下代码运行的结果符合我们的预期(即单线程环境应该的结果),则说这个程序是线程安全的。
线程不安全:
多个线程执行共享变量的操作:同为读操作则不会发生安全问题,至少有一个线程处于写操作则会发生安全问题。
导致线程不安全的原因:
-
不具有原子性:原子性是多行指令是不可拆分的最小执行单位,但一条Java语句不一定是原子的(可以拆分为多条语句)。
-
不满足可见性:一个线程修改共享变量后,能够及时的被其它线程看到称为可见性,而共享变量处于主内存中,线程修改共享变量的过程是:
- 从主内存拷贝一份共享变量到工作内存
- 在工作内存对共享变量进行修改
- 将修改后的共享变量写回主内存
所以,当线程A修改后,还未写回主内存时,线程B从主内存取到的数据是未修改的数据。
-
代码顺序性:指令重排序是单线程环境下JVM和CPU会优化代码执行顺序,而多线程环境下修改执行顺序可能导致线程安全问题的发生。
解决方式1:Volatile关键字
使用Volatile修饰某个变量(实例变量,静态变量)
Volatile关键字作用
:
- 保证可见性:多个线程对同一个变量具有可见性
- 禁止指令重排序:建立内存屏障
- 不保证原子性操作:只能保证共享变量的读读操作的安全和多读一写操作的安全,如果存在多个写进程则不可以保证线程安全。
解决方式2:Synchronized关键字
Synchronized作用:
基于对象头加锁操作,满足原子性,可见性,有序性
- 互斥:满足原子性(某个线程执行同一个对象加锁的同步代码,排斥另一个线程加锁,满足最小执行单位)
- 刷新内存:synchronized结束释放锁会把工作内存的数据刷新到主存,其它线程申请锁始终得到最新数据(满足可见性)
- 有序性:某个线程执行一段同步代码,不管如何重排序,过程中不可能有其它线程执行指令
- 可重入:Synchronized为可重入锁,不会出现将自己锁死的问题,同一个线程可以多次申请同一个对象锁
同步代码块:
使用相同的对象进行加锁
public class 同步代码块 {
private static int count=0;
//这个object就充当锁的作用,所有线程都使用此对象加锁
private static Object object=new Object();
public static void main(String[] args) throws InterruptedException {
for(int i=0 ;i <10 ;i ++){
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object){
for(int i=0 ;i <100 ; i++){
count++;
}
}
}
}).start();
}
Thread.sleep(10000);
System.out.println(count);
}
}
同步方法:
-
实例方法的同步监视器是当前类的实例对象
public class 实例同步方法 { public static int count=0; public static void main(String[] args) throws InterruptedException { Runnable runnable=new Runnable() { @Override public void run() { add(); } //实例同步方法,同步监视器为当前类的实例对象 就是runnable对象 public synchronized void add(){ for(int i=0;i<100;i++){ count++; } } }; for(int i=0;i<10;i++){ //线程共用一个runnable保证同步监视器相同 new Thread(runnable).start(); } Thread.sleep(1000); System.out.println(count); } }
-
静态方法的同步监视器是当前类对象
public class 静态同步方法 {
public static int count=0;
//静态同步方法,同步监视器为当前类对象 静态同步方法.class
public static synchronized void add(){
for(int i=0; i <100 ; i++){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0 ;i <10 ;i++){
new Thread(new Runnable() {
@Override
public void run() {
add();
}
}).start();
}
Thread.sleep(10000);
System.out.println(count);
}
}
读操作使用volatile关键字修饰,写操作加锁-满足线程安全,效率高
多线程通信:
一个线程以通知的方式唤醒某些等待线程,也可以在某些条件下让当前线程等待,这样就能让线程通过通信的方式达到一定顺序性。
前提条件:
必须在加锁的条件下使用
线程通信API | 说明 |
---|---|
wait() | 释放锁,当前线程等待 |
notify() | 通知的方式唤醒一个线程 |
notifyAll() | 通知的方式唤醒全部线程 |
通信例子:
public class 线程通信 {
//库存
public static volatile int COUNT=0;
//锁
public static Object lock=new Object();
public static void main(String[] args) {
//生产任务,当库存达到100时停止生产
Runnable runnable1=new Runnable() {
@Override
public void run() {
while (true){
synchronized (lock){
if(COUNT<100){
COUNT++;
System.out.println("生产一个产品,产品数量为"+COUNT);
lock.notifyAll();
}else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
//5个生产者进程
for(int i=0 ; i <5 ;i++){
new Thread(runnable1).start();
}
//消费任务,库存小于等于0停止消费
Runnable runnable2=new Runnable() {
@Override
public void run() {
while (true){
synchronized (lock){
if(COUNT>0){
COUNT--;
System.out.println("消费一个产品,产品数量为"+COUNT);
lock.notifyAll();
}else{
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
//10个消费者进程
for(int i=0 ;i <10 ;i++){
new Thread(runnable2).start();
}
}
}
4:阻塞队列和线程池
阻塞队列:
线程安全的数据结构,满足队列的特性先进先出
- 队列满的时候继续入队会阻塞,直到有线程从队列中取走元素
- 队列空的时候继续出队会阻塞,直到有线程往队列中放入元素
- 阻塞队列经典的应用场景就是生产者消费者模型
阻塞队列用于生产者消费者模型:
- 生产者和消费者不直接通讯,而是通过阻塞队列进行通讯,生产者生产的产品放到阻塞队列中,消费者从阻塞队列中取产品。(削峰处理)
- 阻塞队列相当于一个缓冲区,平衡生产者和消费者的平衡,并使得生产者和消费者解耦。
Java提供的阻塞队列:
BlockingQueue
常用实现类:
- LinkedBlockingQueue
- ArrayBlockingQueue
线程池:
初始化线程池的时候,就创建一定数量的线程(从线程池的阻塞队列中取任务)
线程池优势:
线程的创建和销毁会有一定开销,使用线程池可以重复使用线程执行多组任务。
Java原生线程池:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
//参数解释
int corePoolSize:核心线程数
int maximumPoolSize:线程总数
long keepAliveTime: 允许空闲的时间
TimeUnit unit:keepAliveTime的单位秒,分等
BlockingQueue<Runnable> workQueue:传递任务的阻塞队列
ThreadFactory threadFactory:创建线程工厂,参与具体线程创建工作
RejectedExecutionHandler handler:拒绝策略,如果任务超过负荷该如何处理
拒绝策略:
CallerRunsPolicy:调用者负责处理(那个线程提交的任务,该线程自己处理)
AbortPolicy:超过负荷抛出异常(默认拒绝策略)
DiscardPolicy:丢弃新来的任务
DiscardOldestPolicy:丢弃最老的任务(丢弃阻塞队列中最老的任务)
自定义拒绝策略:
实现RejectedExecutionHandler
接口即可。
public class 自定义拒绝策略 implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程池工作流程:
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor=
new ThreadPoolExecutor(
5,//核心线程数5
10,//总线程数10
60,//线程过期时间60s
TimeUnit.SECONDS,
//阻塞队列最多放10个任务
new ArrayBlockingQueue<Runnable>(10),
//不写线程工厂
//拒绝策略是谁提交谁处理
new ThreadPoolExecutor.CallerRunsPolicy());
//提交25个任务,当所有线程以及阻塞队列都满了,由main线程执行后面提交被拒绝的任务
for(int i=0 ; i<25;i++){
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("提交任务");
try {
Thread.sleep(100000000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
自定义简单线程池:
public class MyPool {
private BlockingQueue<Runnable> blockingQueue;
/**
*
* @param count 线程总数
* @param capacity 阻塞队列容量
*/
public MyPool(int count,int capacity) {
this.blockingQueue = new ArrayBlockingQueue<>(capacity);
for(int i=0;i<count;i++){
new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
//一直从阻塞队列获取任务
Runnable take = blockingQueue.take();
//执行任务
take.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
//提交任务
public void execute(Runnable runnable){
try {
blockingQueue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}