线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
start()
方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;sleep()
方法不会释放对象锁,wait()
方法会释放对象锁;sleep()
方法属于Thread类而wait()方法属于Object类;sleep()
方法调用后会自动苏醒。wait()方法调用后不会自动苏醒,需要通过调用同一对象的notify()
/notifyAll()
方法唤醒。或者可以使用 wait(long timeout)
超时后线程会自动苏醒。synchronized
关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized
属于重量级锁,JDK1.6之后对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。想要更深入了解synchronized
锁升级,可以看下这篇博客。
说到synchronized
通常就会提到双检锁实现的单例,下面给出具体实现的代码:
public class Singleton{
//使用volatile修饰,方式jvm在实例对象时进行指令重排
private volatile static Sigleton instance;
private Singleton(){
}
public static Singleton getInstance(){
//判断对象是否实例化,未实例化才进入加锁代码继续判断,可以避免不必要的锁获取
if(instance == null){
synchronized(Sigleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
在了解volatile
关键字之前,我们需要简单的了解下java的内存模型。在jdk1.2之前,java的内存模型是直接从主存(线程共享) 中进行读写的。但是在当前的java内存模型下,线程是可以把变量保存到本地内存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为volatile
,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
所以说, volatile
关键字的主要作用就是保证变量的可见性(每次读写都直接操作主内存)然后还有一个作用是防止指令重排序。
volatile
只能修饰变量,synchronized
可以修饰方法和代码块volatile
关键字不会发生阻塞,而synchronized
关键字可能会发生阻塞volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。ThreadLocal
提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
ThreadLocal
的静态内部类ThreadLocalMap
为每个Thread都维护了一个数组table,ThreadLocal
确定了一个数组下标,而这个下标就是value存储的对应位置。
ThreadLocal
使用方式:
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
THREAD_LOCAL.set("");//存
THREAD_LOCAL.get();//取
内存泄漏 :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
使用ThreadLocal
产生内存泄露的的主要原因是由于ThreadLocalMap
内部是由一个Entry
数组组成,其中Entry
的key是ThreadLocal
的弱引用,所以jvm在垃圾回收之前会清除Entry
的key,ThreadLocalMap
中就会出现 key 为 null 的 Entry
,就没有办法访问这些 key 为 null 的 Entry 的 value。这些 value 被Entry对象引用,所以value所占内存不会被释放。
为了防止这种情况的发生,当我们在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池的好处:
我们可以通过ThreadPoolExecutor
的构造方法和通过Executor 框架的工具类Executors
来创建线程池。Executors
为我们提供了三种类型的ThreadPoolExecutor:
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors
去创建,而是通过 ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回线程池对象的弊端如下:
FixedThreadPool和 SingleThreadExecutor: 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
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;
}
ThreadPoolExecutor
构造方法中的参数解析:
corePoolSize
:核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize
:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。keepAliveTime
:当线程池中的线程数量大于 corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁.unit
:keepAliveTime
参数的时间单位。threadFactory
:executor 创建新线程的时候会用到。handler
:饱和策略。关于饱和策略这里就不赘述了,感兴趣的可以去了解一下。AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks
包下面。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。示例代码如下:
public class SemaphoreExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
// 一次只能允许执行的线程数量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
test(threadnum);
semaphore.release();// 释放一个许可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
执行 acquire()
方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release()
方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore
只是维持了一个可获得许可证的数量。 Semaphore
经常用于限制获取某种资源的线程数量。
Semaphore 有两种模式,公平模式和非公平模式,可以通过构造方法中的boolean fair
参数来构造。
CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CountDownLatch的两种典型用法:
CountDownLatch
的计数器初始化为n :new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown()
,当计数器的值变为0时,在CountDownLatch
上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。CountDownLatch
对象,将其计数器初始化为 1 :new CountDownLatch(1)
,多个线程在开始执行任务前首先 coundownlatch.await()
,当主线程调用 countDown()
时,计数器变为0,多个线程同时被唤醒。CountDownLatch 的使用示例:
public class CountDownLatchExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
test(threadnum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一个请求已经被完成
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
CyclicBarrier
和 CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch
更加复杂和强大。主要应用场景和 CountDownLatch
类似。
CyclicBarrier 的使用示例:
public class CyclicBarrierExample2 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
CyclicBarrier和CountDownLatch的区别:
CountDownLatch | CyclicBarrier |
---|---|
减计数方式 | 加计数方式 |
计算为0时释放所有等待的线程 | 计数达到指定值时释放所有等待线程 |
计数为0时无法重置 | 计数达到指定值时,计数置为0重新开始 |
调用countDown()方法计数器减一,调用await()方法只进行阻塞,对计数器没有任何影响 | 调用await()方法计数器会加一,若加一后的值不等于构造器的值,则线程阻塞 |
不可重复利用 | 可重复利用 |