创建并启动一个多线程任务
我们一般熟识的创建多线程方式即为继承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并发编程的艺术》

