线程的基本操作
使用多线程来代替多进程进行并发程序的设计,是因为线程的切换和调度的成本要远远小于进程。
线程的基本操作
1.1 新建线程
Thead t1 = new Thread();
t1.start();
线程start()后会新建一个线程并让这个线程执行run()方法。
1.2 终止线程
stop()的问题:
stop()方法会强制终止一个线程,这可能会导致数据不一致的问题,这样做是不安全的。
可以用一个标记变量,用于指示线程是否需要退出。
1.3 线程中断
严格讲,线程中断并不会线程立即退出,而是给线程发送一个通知,告知目标线程希望它退出。至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
与线程中断有关的三个方法:
public void Thread.interrupt(); //中断线程
public boolean Thread.isInterrupted(); //判断是否被中断
public static boolean Thread.interrupted(); //判断是否被中断,并清除当前中断状态
Thread.interrupt()方法是一个实例方法,它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。Thread.isInterrupted()方法也是一个实例方法,它通过检查中断标志位判断当前线程是否被中断。
中断线程时需要在run()中写有中断处理代码。
Thread t1 = new Thread(){
@Override
public void run() {
while (true) {
//中断处理
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted!");
break;
}
Thread.yield();
}
}
}
注意:Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标志位。
package interrupt;
import javax.swing.*;
public class InterruptedTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("interrupted!");
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Interrupted When sleep");
//设置中断状态
Thread.currentThread().interrupt();
}
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
}
1.4 等待(wait)和 通知(notify)
这两个方法在Object类中,当一个对象实例调用了wait()方法后,当前线程就会在这个对象上等待。线程被notify唤醒后,需要获得这个对象的锁才可以继续执行后续代码,而不是立即执行。
public class SimpleWN {
final static Object object = new Object();
public static class T1 extends Thread {
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T1 start!");
try {
System.out.println(System.currentTimeMillis() + ":T1 wait for object");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + ":T1 end!");
}
}
}
public static class T2 extends Thread {
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ":T2 start!notify one thread");
object.notify();
System.out.println(System.currentTimeMillis() + ":T2 end!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread t1 = new T1();
Thread t2 = new T2();
t1.start();
t2.start();
}
}
运行结果:
T1在延迟了两秒后才执行T1 end! 说明T1在被唤醒后仍然在等待获取object的锁,获取之后才执行后续代码。
注意:Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另外一个主要区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。
1.5 挂起(suspend)和继续执行(resume)线程
这两个操作时一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续执行。但这两个方法不推荐使用。
因为suspend()暂停线程时不会释放任何锁资源,如果resume()意外的在其之前执行了,那么该线程就会被永久挂起。
1.6 等待线程结束(join)和谦让(yield)
JDK中有两个join方法:
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。
yield()方法:
public static native void yield();
这是一个静态方法,一旦执行,它会使当前线程让出CPU。如果觉得一个线程不是那么重要,或者优先级比较低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用yield()方法,给予其他重要线程更多的工作机会。
sleep 和 yield的区别:
sleep()会让线程从运行态进入到阻塞态,不会被分配CPU时间片,而yield()方法可以主动让出CPU,它使线程从运行态进入到就绪状态,仍然有机会被处理机调度。
初识volatile
当使用volatile关键字声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。
package volatileTest;
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
System.out.println(number);
}
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
number = 42;
ready = true;
Thread.sleep(10000);
}
}
给ready加了volatile关键字后,告诉Java虚拟机,这个变量可能会在不同的线程中修改:
线程安全和关键字synchronized
1、线程安全
线程安全是并行程序的根基。volatile关键字并不能真正确保线程安全,它只能确保一个线程修改了数据后,其他线程能看到这个改动,但当两个线程同时修改某一个数据时,依然会产生冲突。
2、关键字synchronized
关键字synchronized的作用是实现线程间的同步。
它的工作是对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证线程间的安全性。
关键字synchronized有多种用法:
- 指定加锁对象:对给定对象加锁,进入同步代码块前需要获得给定对象的锁
- 直接作用于实例方法:相当于对当前实例方法加锁,进入同步代码块之前要获得当前实例的锁
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码块之前要获得当前类的锁。
关键字synchronized还可以保证线程间的可见性和有序性,从可见性角度上讲,关键字synchronized可以完全替代关键字volatile的功能,但使用上没有那么方便。就有序性而言,由于关键字synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语句一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁之后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,有序性问题自然得到了解决。
多线程下的ArrayList和HashMap
1、ArrayList
由于容器ArrayList不是线程同步的,所以如果多个线程同时访问一个ArrayList,就会出现数据的冲突。
解决办法是用Vector代替ArrayList
2、HashMap
HashMap也不是线程安全的,在JDK8之前,如果多个线程同时向HashMap中添加数据,则可能会造成死循环,这是因为破坏了HashMap内部的结构,将链表变成了环,即两个节点互相指向对方,但在JDK8之后解决了这个问题,但仍然不能再多线程环境下使用HashMap,可以用ConcurrentHashMap代替。
错误的加锁
static Integer i = 0;
increase() {i++}
synchronized(i) {
increase();
}
这样的锁,仍然不会使得线程安全,因为把锁加在了变量i上,而Integer属于不变对象,即对象一旦创建,就不可能被修改。也就是说,如果你有一个Integer对象代表1,那么它就永远代表1,你不可能修改Integer对象的值,使它为2,如果需要2,则必须重新创建一个Integer对象,并让它来表示2。
反编译后会发现i++实际上是在执行i =Integer.valueOf(i.intValue() + 1);,Integer.valueOf()是一个工厂方法,它会倾向于返回一个代表指定数值的Integer对象实例。因此i++的本质是创建一个新的Integer对象,并将它的引用赋值给i。
由于在多个线程间,并不一定能够看到同一个i对象(因为i对象在不断地改变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码块控制出现问题。