1. 线程安全
多个线程在访问同一个对象的时候不需要其他额外的同步手段或措施就能保证该对象被正确的访问并产生正确的执行结果。那么这个对象就是线程安全的。
线程安全的代码必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步),使用该代码的开发人员无需关心多线程的问题也不用自己采用任何措施来保证多线程的正确调用。
线程不安全的代码在多个线程中使用时必须作同步处理,否则可能产生不可预期的后果。
2. Java中的线程安全
可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
2.1 不可变
不可变(Immutable) 的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。
只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
不可变的类型:
- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和AtomicLong 则是可变的。
如果共享数据是一个基本数据类型,只需要在定义的时候声明为final即可;
如果是共享数据是一个对象,则需要保证对象的行为不会对其状态产生任何影响才行(最简单的做法就是把对象中带有状态的变量都声明为final)。
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
public class ImmutableExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("a", 1);
}
}
Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
2.2 绝对线程安全
不管运行环境如何,调用者都不需要任何额外的同步措施的类可以称作是绝对线程安全的,但是这通常是需要付出相对较大的代价的。
2.3 相对线程安全
对这个对象单独的操作是线程安全,在调用单个操作的时候不需要做其他额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
总之相对线程安全就是多个线程对这个对象单独操作的时候是线程安全的,但是如果多个线程操作这个对象的不同行为时就需要调用端使用同步的手段来保证调用的正确顺序了。
在Java语言中,大部分的线程安全类都是属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection()等。
对于下面的代码,如果删除元素的线程删除了 Vector 的一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出ArrayIndexOutOfBoundsException。
public class VectorUnsafeExample {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 100; i++) {
vector.add(i);
}
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
});
executorService.execute(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
});
executorService.shutdown();
}
}
}
如果要保证上面的代码能正确执行下去,就需要对删除元素和获取元素的代码进行同步:
executorService.execute(() -> {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
executorService.execute(() -> {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
});
2.4 线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
如Vector和HashTable相对应的集合类ArrayList和HashMap等。
2.5 线程对立
线程对立是指物理调用端是否采取了同步措施,都无法再多线程环境中并发使用的代码。
3. 守护线程
Java提供了两种线程:守护线程和用户线程
守护线程又称“服务进程”、“精灵线程”或“后台线程”。
是指在程序运行时再后台提供的一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。
- 任何一个守护线程都是整个JVM中所有非守护线程的“保姆”
3.1 守护线程和用户线程唯一不同点
如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了。
只要有任何的用户线程还在运行,JVM就不会终止。
3.2 自己设置守护线程
Java语言中,守护线程一般具有较低的优先级,并非只由JVM内部提供,用户在编写程序时也可以自己设置守护线程。
如将一个用户线程设为守护线程的方法:即,在调用start()方法启动线程之前调用对象的setDaemon(true)方法。
若以上参数设置为false,则表示是用户进程模式。
注意:当在一个守护线程中产生了其它线程,那么这些新产生的线程默认还是守护线程,
class ThreadDemo extends Thread{
public viod run(){
System.out.println(Thread.currentThread().getName()+":begin");
try{
Thread.sleep(1000);
}catch(InterruptedExceptioin e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":end");
}
}
public class Test{
public static void main(String[] args){
System.out.println("test:begins");
Thread t1=new Thread();
t1.setDeamon(true);
t1.start();
System.out.println("test:end");
}
}
test:begin
test:end
Thread-0:begin
未输出Thread-0:end,由于在启动线程前把它设为守护线程了。当程序中只存在守护线程时,JVM是可以退出的。
因此当test方法调用结束后,mian线程将退出,此时线程t1还出与休眠状态,没有运行结束。但由于JVM中只剩守护线程了,JVM会自动关闭。
3.3 守护线程的例子
典型例子就是垃圾回收器。
只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。
4. join()
作用:让调用该方法的线程在执行完run()之后,再执行join()后面的代码。
简单来说:就是将两个线程合并,用于实现同步功能。
具体:可以通过线程A的join()开等待线程A的结束,或者使用线程A的join(2000)方法来等待A的结束,
public class Test{
public static void main(String[] args){
Thread t=new Thread(new ThreadImp());
t.start();
try{
t.join();
if(t.isAlive())//t已经结束
System.out.println("t has not finished");
else
System.out.println("t has finished");
System.out.println("joinFinish");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
class ThreadImp implments Runnable{
public void run(){
try{
System.out.println("Begin ThreadImp");
Thread.sleep(5000);
System.out.println("End ThreadImp");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
Begin ThreadImp
t has finished
joinFinish
End ThreadImp