线程间的通信机制:
举个例子,多线程打印十次ABC后打印OVER,然后退出。用各种通信机制实现一遍。
1)volatile和synchronized关键字等。都是借助指令来保证了所有线程对关键字修饰的共享变量的可见性。这边以ReantranLocak为例。
public class Test3 {
private static volatile int abc = 0;
private static final Lock LOCK = new ReentrantLock();
private static final Condition A_CONDITION = LOCK.newCondition();
private static final Condition B_CONDITION = LOCK.newCondition();
private static final Condition C_CONDITION = LOCK.newCondition();
private static final Condition OVER_CONDITION = LOCK.newCondition();
public static void main(String[] args) throws Exception {
new Thread(new PrintAThread()).start();
new Thread(new PrintBThread()).start();
new Thread(new PrintCThread()).start();
new Thread(new PrintOverThread()).start();
new Thread(() -> {
LOCK.lock();
try {
A_CONDITION.signal();
} finally {
LOCK.unlock();
}
}).start();
}
static class PrintAThread implements Runnable {
@Override
public void run() {
LOCK.lock();
try {
while (abc < 30) {
A_CONDITION.await();
System.out.print("A");
abc++;
B_CONDITION.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
static class PrintBThread implements Runnable {
@Override
public void run() {
LOCK.lock();
try {
while (abc < 30) {
B_CONDITION.await();
System.out.print("B");
abc++;
C_CONDITION.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
static class PrintCThread implements Runnable {
@Override
public void run() {
LOCK.lock();
try {
while (abc < 30) {
C_CONDITION.await();
System.out.println("C");
abc++;
if (abc != 30) {
A_CONDITION.signal();
}
}
OVER_CONDITION.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
static class PrintOverThread implements Runnable {
@Override
public void run() {
LOCK.lock();
try {
OVER_CONDITION.await();
System.out.println("OVER");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
}
2)等待通知机制。通过wait()/notify()/nodifyAll()方法实现。
public class Test1 {
private static volatile int abc = 1;
public static void main(String[] args) {
PrintThread printA = new PrintThread(1, "A");
PrintThread printB = new PrintThread(2, "B");
PrintThread printC = new PrintThread(0, "C");
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
Future futureA = cachedThreadPool.submit(printA);
Future futureB = cachedThreadPool.submit(printB);
Future futureC = cachedThreadPool.submit(printC);
try {
futureA.get();
futureB.get();
futureC.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("OVER");
cachedThreadPool.shutdown();
}
static class PrintThread implements Runnable{
private int value;
private String printChar;
PrintThread(int value, String printChar) {
this.value = value;
this.printChar = printChar;
}
@Override
public void run() {
int count = 0;
while (count < 10) {
synchronized (Test1.class) {
if (abc % 3 == value) {
if ("C".equals(printChar)) {
System.out.println(printChar);
} else {
System.out.print(printChar);
}
abc++;
count++;
Test1.class.notifyAll();
} else {
try {
Test1.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
3)管道输入/输出流。用管道来打印ABC跑的也忒慢了。。。呜呜呜,而且还有点小问题,有没有大佬指点一下。
public class Test {
private static volatile AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
PipedWriter writerA = new PipedWriter();
PipedReader readerA = new PipedReader();
PipedWriter writerB = new PipedWriter();
PipedReader readerB = new PipedReader();
PipedWriter writerC = new PipedWriter();
PipedReader readerC = new PipedReader();
try {
writerA.connect(readerA);
writerB.connect(readerB);
writerC.connect(readerC);
Thread printThreadA = new Thread(new Print(readerC, writerA, "A"));
Thread printThreadB = new Thread(new Print(readerA, writerB, "B"));
Thread printThreadC = new Thread(new Print(readerB, writerC, "C"));
printThreadA.start();
printThreadB.start();
printThreadC.start();
writerC.write(0);
printThreadA.join();
printThreadB.join();
printThreadC.join();
System.out.println("OVER");
} finally {
writerA.close();
writerB.close();
writerC.close();
}
}
static class Print implements Runnable {
private PipedReader reader;
private PipedWriter writer;
private String printChar;
Print(PipedReader reader, PipedWriter writer, String printChar) {
this.reader = reader;
this.writer = writer;
this.printChar = printChar;
}
@Override
public void run() {
try {
while (reader.read() != -1) {
if (count.intValue() >= 30) {
break;
}
count.getAndAdd(1);
if ("C".equals(printChar)) {
System.out.println(printChar);
} else {
System.out.print(printChar);
}
writer.write(0);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4)Thread.join()的使用
5)ThreadLocal
先说说ThreadLocal的数据结构。在ThreadLocal类中有一个静态内部类ThreadLocalMap,而ThreadLocalMap有一个成员属性是Entry数组,Entry是ThreadLocalMap类中的一个静态内部类。由Entry类的构造函数可以看出,一个Entry对象至少有两个成员属性:ThreadLocal<?> k 和Object value。也就是说,一个Thread中只有一个ThreadLocalMap对象,一个ThreadLocalMap对象可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry。
static class ThreadLocalMap {
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
使用场景:每个线程需要有自己单独的实例;实例需要在多个方法中共享,但不希望被多线程共享。
提一提Entry的父类WeakReference,也就是弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是value 是强引用,不会被清理,就会出现 key 为 null而value还在的Entry对象。为了避免内存泄漏等情况出现,在调用ThreadLocalMap类的set()、get()、remove()方法时,都会调用一个叫做expungeStaleEntry()的方法,用来清理掉key为null的Entry对象。但这仅限于ThreadLocal没有被外部强引用的情况下。如果有自定义的ThreadLocal对象存在,最好手动调用remove()方法回收该对象,尤其在线程池场景下,线程经常会被复用,可能会影响后续业务逻辑和造成内存泄露等问题。
线程池技术
通过线程的复用,消除频繁创建和消亡线程的系统资源开销,面对过量任务的提交也能平缓恶化。
Executors提供了四种常用的线程池,CacheThreadPool、FixedThreadPool、SingleThreadExecutor、ScheduledThreadPool,这里不单独做介绍,而是介绍他们的父类ThreadPoolExecutor。因为这四种常用线程池实际上都是通过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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池的七个参数详解:
1)corePoolSize:核心线程数。当有任务进来的时候,如果当前线程池内线程的数量小于核心线程数,即使有空闲线程,也不会复用,而是新建一个线程来执行传入的任务。核心线程一般不会销毁,除非通过allowCoreThreadTimeOut(boolean value)方法设置allowCoreThreadTimeOut参数为true,给核心线程添加超时机制,超时了才会销毁。
2)maximumPoolSize:最大线程数。当有任务进来且线程池内线程数不小于核心线程数且无空闲线程时,则会创建一个临时线程来执行该任务。临时线程有超时机制,超时则销毁。
3)keepAliveTime:超时时间。与超时机制对应,当线程空闲时间达到超时时间时,则销毁该线程。
4)unit:超时时间的单位,比如说秒,毫秒。
5)workQueue:阻塞队列。当有任务进来时,任务会先进入到该队列中,调度的时候再从队列中取出来,交给线程执行。这个参数其实一定程度上决定了线程池的运行策略,因为从队列中取出任务的规则会决定线程池执行任务的顺序,而队列的有界和无界更有意思了。如果是无界队列的话,任务可以无限加入到该队列中;如果是有界队列的话,当任务数大于队列长度,那么新来的任务是无法加入队列的,如果没有核心线程来取走它,那么会在线程池中创建一个临时线程来执行该任务。
6)threadFactory:用来创建线程的工厂。ThreadFactory是一个接口,通过该接口可以自定义实现生成线程的方式、定义线程名格式、是否后台执行等等,通常是用来定义线程名称的格式,用来排查问题。
7)handler:拒绝策略。该策略只对有界队列有效。当线程池内线程数达到了最大线程数且都不空闲,而阻塞队列也放满了任务,那么就需要一个策略来应对新来的任务。
线程池添加任务的方式:
由参数部分可以知道,若线程池内的线程想要执行任务,那么任务一定是从阻塞队列中取出来的。而线程池启动后,只有通过调用execute(Runnable commond)方法才能将任务放入队列中。在ThreadPoolExecutor类中,还有一个submit方法,该方法有三种传参方式,但是在方法内都调用了execute方法将任务添加到队列中,submit方法拓展的功能就是,能够得到一个Future<T>类型的返回值。Future是一个接口类,通过返回的Future对象,可以对对应的线程做取消/join操作和状态判定。
public interface Future<V> {
/**
* 试图取消当前任务的执行。如果任务已经执行完或已经被取消或因为某些原因无法取
* 消,这次操作则会失败。如果任务尚未开始,那这个任务不会再执行了。如果任务正
* 在运行,通过传参来决定是否需要中断线程来取消任务。
*
* @param mayInterruptIfRunning 传参的值为true的话,允许尝试中断线程来取消。
* @return 是否取消成功。
*/
boolean cancel(boolean mayInterruptIfRunning);
/**
* @return 指定任务是否取消成功。
*/
boolean isCancelled();
/**
* @return 指定任务是否已完成。
*/
boolean isDone();
/**
* 阻塞当前线程,直到执行该任务的线程执行完毕,并返回任务执行的返回值。
*/
V get() throws InterruptedException, ExecutionException;
/**
* 阻塞当前线程,直到执行该任务的线程执行完毕,并返回任务执行的返回值。如果阻塞
* 时间达到传参设定的超时时间,抛出超时异常。
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
通常情况下,线程池内一个工作线程完整的执行流程如下:
execute(提交任务到阻塞队列中) => addWorker(创建一个Worker对象,该对象有一个成员变量是Thread,执行thread.start()方法) => runWorker(先取Worker对象的firstTask成员变量,如果为空,则调用getTask()方法去队列中取优先级最高的任务,取得任务且一系列状态判断通过后,执行该任务。工作线程不断循环该过程,直到Worker对象的firstTask成员变量为空且getTask()方法取到了空值) => processWorkerExit(通过该方法来管理线程池内的线程数,以及是否需要关闭线程池,如果Worker是异常结束的话,则立马addWorker) => tryTerminate(满足终结线程池条件且阻塞队列中任务数为0时,关闭线程池)
手动关闭线程池:
手动关闭线程池有shutdown()和shutdownnow两个方法。shutdown()方法是将线程池状态置为shutdown状态,然后中断空闲线程,等正在运行的线程执行完毕后再关闭线程池;shutdownnow()方法是将线程池状态置为stop状态,并将所有存活的线程中断,再返回队列中尚未执行的任务列表。