多线程
1.进程,线程
- 进程:是程序的一次执行过程,即一旦程序被载入到内存并准备运行,它就是一个进程。
进程是表示资源分配的基本单位 - 线程:线程是进程执行的最小单位,一个进程在其执行的过程中可以产生多个线程。同一进程下的线程共享进程的资源
线程是系统调度的基本单位
1.什么是死锁
所谓死锁(Deadlock),是指多个进程在运行过程中因争夺资源而造成的一种僵局(DeadlyEmbrace),当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
产生死锁的四个条件
- 互斥条件
- 请求并保持
- 不剥夺条件
- 循坏等待
2.处理死锁的基本方法
(1) 预防死锁 :这是一种较简单和直观的事先预防的方法。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来预防发生死锁。
预防死锁是一种较易实现的方法,已被广泛使用。但由于所施加的限制条件往往太严格,因而可能会导致系统资源利用率和系统吞吐量降低。
(2) 避免死锁(银行家算法):该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。这种方法只需事先施加较弱的限制条件,便可获得较高的资源利用率及系统吞吐量,但在实现上有一定的难度。目前在较完善的系统中常用此方法来避免发生死锁。
(3) 检测死锁(资源分配图):这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,而是允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源; 然后,采取适当措施,从系统中将已发生的死锁清除掉。
(4) 解除死锁 :这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤消或挂起一些进程,以便回收一些资源
如何避免线程死锁
- 破坏互斥条件:无
- 破坏请求并保持:一次性地申请所有用到的资源
- 破坏不剥夺条件:占用部分资源的线程申请其他资源是时,如果申请不到。主动释放它占有的资源
- 破坏循坏等待条件:按序申请资源
2. java线程相关
线程的创建方式:
- 自定义类继承Thread父类,重写 run()方法
- 自定义类实现 Runable接口,将实现类作为 new Thread() 时的参数
- 自定义类实现 Callable 接口,再使用 FutureTask 封装 Callable,将FutureTask 作为 new Thread()时的参数
- 使用线程池,创建好之后,将自定义的Runable 实现类/ Callable 实现类作为参数提交(Runable 用 .execute(),Callable 用 .submit())
sleep(), wait() 比较
相同:
两者都可以暂停线程的执行
区别:
- sleep()属于Thread类下的方法,没有释放锁,sleep()执行完之后线程自动苏醒
- wait()属于Object 类下的方法,释放了锁,wait()调用之后,线程不会自动苏醒,必须等别的线程调用同一锁对象上的.notify() 或者
.notifyAll() ⽅法- wait ()通常被⽤于线程间交互/通信,sleep ()通常被⽤于暂停执⾏
synchronized 关键字
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。
synchronized 修饰,一开始是使用重量锁,是依赖对象内部的monitor对象锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以阻塞线程时,需要从用户态到内核态的切换,由内核线程来完成;同样线程唤醒时,也必须依靠内核线程来完成。
用户态到内核态的之间的转态转换,需要进行系统中断,保护和恢复执行上下文信息,这些都会影响程序的性能。
所以JDK6之后有了锁优化,即无锁->偏向锁->轻量级锁->重量级锁的锁升级机制。
volatile关键字
指示 JVM,这个变量是不稳定的,每次使⽤它都到主存中进⾏读取
volatile 关键字的主要作⽤就是
- 保证变量的可⻅性 ;保证变量在线程间可见,对volatile变量所有的写操作都能立即反应到其他线程中
- 防⽌指令重排序
底层原理
volatile 修饰的变量编译成汇编代码后,其相对普通变量的赋值操作指令后,多了一行lock addl $0x0,(%esp)
, lock前缀的作用是将本处理器的缓存写入内存,该写入动作也会引起其他处理器或其他内核无效化其缓存。
相当于进行了 assign -> store -> write 三个操作,即Java 内存模型定义的第一条特殊规则
再加上Java 内存模型对于Volatile 型变量定义的第二条特殊规则,即必须是read -> load -> use,实现了volatile 型变量的可见性。
Java内存模型定义的第三条特殊规则实现了防止指令重排的功能。
1.Thread和Runnable方式的区别
- Runable 是接口,Thread实现了Runable
- Runnable 的可以理解为是任务,没有线程的功能。
Thread 是具有线程创建的功能,其中的start()方法是创建线程的关键。start()方法会调用一个start0()方法,这个方法是一个native方法,由jvm去启动一个线程,jvm启动线程完毕后,通过回调调用Thread的run()方法。- 直接调用run方法就是一个运行在main线程的普通方法
2. java线程池
创建线程池的方式
- 使用类 Executors来实现 我们可以创建四种类型的
ExecutorService executorService = Executors.newCachedThreadPool();
//适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
ExecutorService executorService = Executors.newSingleThreadExecutor();
//SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景
ExecutorService executorService = Executors.newFixedThreadPool();
ExecutorService executorService = Executors.newScheduledThreadPool(int corePoolSize);
- 通过 ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池的7个参数
- corePoolSize: 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize
- maximumPoolSize : 线程池的最大线程数量
- keepAliveTime :空闲线程存活时间
一个线程处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定的这个时间之后,线程会被销毁。
- unit 空闲线程存活时间的单位
- workQueue 工作队列:通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。
其采用阻塞队列实现 (BlockingQueue< Runnable > workQueue),Java 提供了 BlockingQueue 接口的7 种阻塞队列的实现。以下是 Executors 封装的四种功能线程池的 workQueue 的使用。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())); //基于链表的阻塞队列,同ArrayListBlockingQueue类似,
//其内部也维持着一个数据缓冲队列(该队列由一个链表构成)
//如果构造一个LinkedBlockingQueue对象,而没有指定其容量
//大小,LinkedBlockingQueue会默认一个类似无限大小的容量
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()); //SynchronousQueue是一个内部只能包含一个元素的队列。
//插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了
//队列中存储的元素
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue()); //DelayedWorkQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
//DelayedWorkQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)
//永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞
}
- threadFactory(可选):线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂
- handler(可选):拒绝策略。当达到最大线程数时需要执行的拒绝策略。
ThreadPoolExecutor 已经为我们实现了 4 种拒绝策略:
AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy:由调用线程处理该任务。
DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
线程池总结
- 为什么使用线程池?
(1) 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
(2) 提高响应速度:任务到达时,无需等待线程创建即可立即执行
(3) 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
3.Atomic 原⼦类
Atomic 是指⼀个操作是不可中断的。即使是在多个线程⼀起执⾏的时候,⼀个操作⼀旦开始,就不会被其他线程⼲扰。
主要利⽤ CAS (compare and swap) + volatile 和 native ⽅法来保证原⼦操作
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe U = Unsafe.getUnsafe();
private volatile int value; // volatile 修饰, 保证变量在线程间的可见性
// objectFieldOffset(AtomicInteger.class, "value") :
// Reports the location of the field with a given name in the storage
// allocation of its class.
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
public AtomicInteger(int initialValue) {
value = initialValue;
}
// @param newValue the new value
// @return the previous value
public final int getAndSet(int newValue) {
return U.getAndSetInt(this, VALUE, newValue);
}
public final class Unsafe {
private static native void registerNatives();
static {
registerNatives();
}
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
return theUnsafe;
}
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset); // 一个 Native 方法
} while (!weakCompareAndSetInt(o, offset, v, newValue)); // CAS 方式
return v;
}
}
4.Threadocal变量
ThreadLocal变量是用来在多线程中实现数据隔离的,每个线程都有一个ThreadLocal变量的副本,填充的数据只属于当前线程,防止在多线程的环境中自己的变量被其他线程篡改。
底层原理
线程中使用ThreadLocal 变量的步骤
set(T value):线程设置该ThreadLocal对应的变量
get():线程获取该ThreadLocal对应的变量
remove():线程移除该ThreadLocal
在set() 的时候,首先会获取当前线程Thread类中定义的一个字段 threadLocals,
threadLocals 是一个ThreadLocalMap类型的字段,这个类型类似HashMap,底层结构是一个Entry<K,V>类型的数组
得到了threadLocals这个map结构之后,把自己当做key,调用threadLocals.set(key,value)。
使用场景有哪些?
Android 中的消息分发机制中,定义是一个线程只能有一个Looper对象,利用的就是ThreadLocal变量来实现的Looper的线程唯一性(因为ThreadLocalMap底层是一个Entry<k,v>数组,所以一个线程一个key只能有一个value,即Looper对象)
volatile, Atomic Demo
public class VolatileModel {
static int count = 0;
volatile static int count1 = 0;
static AtomicInteger count2 = new AtomicInteger(0);
public void increment() {
count ++;
count1 ++;
count2.incrementAndGet();
}
public class myRunable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 1000; i++)
increment();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
VolatileModel model = new VolatileModel();
Thread thread1 = new Thread(model.new myRunable());
Thread thread2 = new Thread(model.new myRunable());
thread1.start();
thread2.start();
while(Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
System.out.println(count1);
System.out.println(count2);
}
}
method 0001 001F 000B 0001 Attributes {
code 000C 0000004E 0002 0001 00000018 Code {
getstatic B2 000D
iconst_1 04
iadd 60
putstatic B3 000D
getstatic B2 000F
iconst_1 04
iadd 60
putstatic B3 000F
getstatic B2 0017
invokevirtual B6 0020
pop 57
return B1
} 0000 ExceptionTable {
} 0002 Attributes {
lineNumberTable 0019 00000012 0004 Table {
lineNumber 0000 000C
lineNumber 0008 000D
lineNumber 0010 000E
lineNumber 0017 000F
}
localVariableTable 001A 0000000C 0001 Table {
localVariable 0000 0018 001D 001E 0000
}
}
}
当变量使用volatile 修饰时,count1的字节码的访问标志为ACC_STATIC, ACC_VOLATILE,在进行写操作(putstatic )到主存(方法区 ) 的时候,JVM 会执行相应的处理,使得其他使用该变量的线程中的工作内存(操作数栈 )中的值无效,必须重新从主存中取值。
以后有时间再深入的了解JVM源码是怎么实现的吧ヽ(ー_ー)ノ
5.sleep,wait,yield,join
sleep:Thread类中的方法,阻塞当前线程,不会释放锁
wait:Object类的方法,阻塞当前线程,释放锁
yield:Thread类的方法,相对sleep,使得当前线程变为就绪状态
join:Thread类的方法,阻塞当前线程,直到调用join()的线程运行完成
6.Java内存区域,Java内存模型,硬件内存模型
!!!方法区并不等于主内存,操作数栈也并不等于工作内存
Java 内存模型中的主存 可以类比硬件内存模型中的主存
Java 内存模型中的工作内存 可以类比硬件内存模型中的缓存
Java 内存模型中的JVM 可以类比硬件内存模型中的CPU