创建并启动一个多线程任务
我们一般熟识的创建多线程方式即为继承Thread类或是实现Runnable接口,重写run()方法,还有创建线程池实现。
手动定义一个线程任务(作为内部类)的方法现在已经不被提倡,所以遇到可能存在并发的复杂任务时,一般采用线程池来实现。
相关方法
一些设计并发常用并且容易被混淆的方法们:
static sleep() : Thread类的静态方法,阻塞当前正在线程,不释放锁;
wait() : 当前线程暂停,并释放锁且暂时无法重新获得锁,必须绑定当前对象内容锁(如使用Synchronized的同步块),知道其他线程调用notify()/notifyAll()才有机会获得锁继续执行;
yield() : 当前线程暂停,此时时间片分配给其他线程,但是不会分配给优先级更低的线程;
join() : 等待此线程执行完毕后才能执行;
isInterrupted() : 判断此线程是否执行完毕。
有一些停止线程任务的方法已经被弃用,其实,从外部去中断一个线程是很不合理的方式,我们应该使用更加安全的方式去打断一个我们希望被停止运行线程,例如在循环中使用标志位。
synchronized
我们借助synchronized来定义同步方法或是同步块,使用对象锁。有以下几点要注意:
定义的是对象锁,多个同步块拥有同一个对象锁才能被约束;
是可重入锁,抛出异常也会自动释放;
该关键字不会被继承;
通常使用 本类.class(一般用于类锁)、字符串、this、一个既有的非空对象作为锁;
在静态方法中或是.class定义为类锁,锁住的是类,与对象锁并不冲突。
关于volatile关键字
该关键词定义在变量,主要为保证变量变化在主存的可见性保证数据的一致性,这里我们不再介绍jvm内存模型,需要注意的是,有些操作即使不适用volatile也能做到变量同步,例如sleep和输出操作,使用synchronized后我们可以避免使用volatile。
线程间的通信
线程间的通信主要利用管道输入\输出流(PipedOutputStream、 PipedInputStream、PipedWriter、 PipedReader),下面是一个实例:
读线程
public class ThreadRead extends Thread { private ReadData read; private PipedInputStream input; public ThreadRead(ReadData read, PipedInputStream input) { super(); this.read = read; this.input = input; } @Override public void run() { read.readMethod(input); } }
写线程
public class ThreadWrite extends Thread { private WriteData write; private PipedOutputStream out; public ThreadWrite(WriteData write, PipedOutputStream out) { super(); this.write = write; this.out = out; } @Override public void run() { write.writeMethod(out); } }
主方法:
public static void main(String[] args) { try { WriteData writeData = new WriteData(); ReadData readData = new ReadData(); PipedInputStream inputStream = new PipedInputStream(); PipedOutputStream outputStream = new PipedOutputStream(); // inputStream.connect(outputStream); 连接管道两端,皆可 outputStream.connect(inputStream); ThreadRead threadRead = new ThreadRead(readData, inputStream); threadRead.start(); Thread.sleep(2000); ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream); threadWrite.start(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }
※ 关于ThreadLocal
ThreadLocal类是一个泛型类,用于解决多线程中变量共享的问题,可以让线程绑定一个自己独立的静态变量,注意,它的存在是区别于静态变量的,不要与费静态变量混为一谈。相关方法:
get() : 返回当前线程的此线程局部变量的副本中的值;
set(T value) : 将当前线程的此线程局部变量的副本设置为指定的值;
remove() : 删除此线程局部变量的当前线程的值;
initialValue():返回此线程局部变量的当前线程的“初始值”。
其实现原理其实是在一个线程中创建ThreadLocalMap(基于HashMap),借助这种手段定义的静态变量对于每个线程都有其独立的副本,在免去其初始化性能损耗的同时,提供了安全性的操作,可以用于数据库连接管理等情形。
一个简单的例子:
public class Test { public static ThreadLocal<String> str = new ThreadLocal<String>(); public static void main(String[] args) { if (str.get() == null) { System.out.println("为ThreadLocal类对象放入值:str"); str.set("string"); } System.out.println(str.get());//string } }
一个很常见的应用就是工具类里用来定义SimpleDateFormat对象,因为这个对象本身不是线程安全的,容易引起数据串扰或是抛出异常。
内存泄露问题
Map中的key值是ThreadLocal变量的弱引用,仅仅针对key,此时ThreadLocal实例没有强引用指向,将会gc被回收,但是其在Map中的value不会被回收,直到线程结束整体被gc回收,这就存在了内存泄露的问题,尤其是在线程池中,线程不会被回收而是复用。
为了避免这个问题,ThreadLocal在每次执行get(),set()时会将key为null的value回收,但是在线程池的环境中仍会出现问题,对此有解决方案:在每次使用完一个ThreadLocal变量后,执行remove()方法移除变量,使其值为null从而被gc回收,或是直接将其定义为private static。
其他并发常见问题
有效规避死锁
死锁不只存在于数据库中,两个线程相互等待彼此的资源是放也会产生死锁。
死锁的产生条件:
互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
相关链接:线程间通信知识点补充
相关书籍:《Java并发编程的艺术》