Java线程基础
一、创建线程
1、直接创建线程
在Java中线程使用Thread类表示,因此创建线程最简单的方法就是new 一个Thread类:
Thread myThread = new Thread();
myThread.start();
- 1
- 2
线程创建之后,调用它的start()方法就可以让线程运行,线程Thread有一个run()方法,start()方法调用后具体运行的是run()方法中的代码。因此,创建线程时,通常都会重载这个run()方法:
Thread thread = new Thread(){
@Override
public void run(){
System.out.println("Do something");
}
}
thread.start();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
如果不通过start()方法,而是直接调用线程实例的run方法,那么就只是作为一个普通的方法调用。
2、通过Runnable创建线程
除了通过继承Thread类重载run()方法来创建线程外,还可以通过Runnable接口来创建线程。Runnable接口是一个单方法接口,它只有一个run()方法:
public interface Runnable {
public abstract void run();
}
- 1
- 2
- 3
Thread类有一个构造方法,其参数就是Runnable实例:
public Thread(Runnable target)
- 1
如果传入了Runnable实例,那么start()方法调用时,线程就会执行Runnable的run方法。
二、终止线程
通常线程在执行完毕后就会结束,不用明确写出关闭的代码。但Java也提供了一些手动关闭线程的方法,如Thread类的stop()方法,stop()方法可以立即终止一个线程,但现在stop()方法是一个被标注为废弃的方法。因为stop()方法太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
因此如果需要停止一个线程时,可以在编写线程类时,自行决定线程何时退出:
public class MyThread extends Thread{
volatile boolean stop = false;
// 自定义线程停止方法
public void stopThread(){
stop = true;
}
@Override
public void run(){
while(true){
if(stop){
System.out.println("Thread end by stopThread");
break;
}
System.out.println("Thread is running");
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
三、线程中断
线程中断是一种重要的线程协作机制,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程退出!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
与线程中断有关的,有三个方法:
public void Thread.interrupt() // 中断线程
public boolean Thread.isInterrupted() // 判断是否被中断
public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态
- 1
- 2
- 3
如果希望线程在中断后做相应处理或终止线程,就可以通过isInterrupted()为它增加相应的中断处理代码:
Thread thread = new Thread(){
@Override
public void run(){
while(true){
// 判断线程是否已被中断
if(Thread.currentThread().isInterrupted()){
System.out.println("Interruted!");
break;
}
System.out.println("Thread is running");
}
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
线程的sleep()方法
Thread.sleep()方法会让当前线程休眠若干时间(单位毫秒),但不会释放任何资源。当线程在休眠时如果被中断,就会抛出InterruptedException异常,这个异常不是运行时异常,程序必须捕获并且处理它:
public static void main(String[] args) throws InterruptedException{
Thread myThread = new Thread(){
@Override
public void run(){
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("Interruted!");
// 终止线程
break;
}
try{
// myThread休眠2秒
Thread.sleep(2000);
} catch(InterruptedException e){
System.out.println(" Thread was Interrupted when sleep");
// 设置中断状态,sleep抛出中断异常时,它会清除中断标记,
// 如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,
// 所以在异常处理中,需再次设置中断标记位。
Thread.currentThread().interrupt();
}
}
}
};
myThread .start();
// 主线程休眠两秒
Thread.sleep(2000);
// 中断myThread
myThread .interrupt();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
之所以主线程也要休眠,是因为如果不休眠,myThread可能还没有进入休眠状态就被主线程中断了。
四、wait和notify
wait()和notify()方法是Object类中的两个方法,主要用于线程之间的协作:
public final void wait() throws InterruptedException
public final native void notify()
- 1
- 2
当在一个对象上调用wait()方法后,当前线程就会在这个对象上等待,一直到其他线程调用了这个对象的notify()方法为止。具体来说,如果一个线程调用了object.wait(),那么这个线程就会进入object对象的等待队列,当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程,并将其唤醒。除了notify()方法外,Object对象还有一个类似的notifyAll()方法,它和notify()的功能基本一致,但不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。
需要注意的是,wait()方法和notify()方法并不是在任何地方都可以调用,必须包含在synchronized语句块中,也就是说,wait()和notify()方法在执行前必须获得synchronized中的对象锁。
public class WaitAndNotifySample{
final static Object object = new Object();
public static class TestThreadA extends Thread{
@Override
public void run(){
synchronized(object){
System.out.println(System.currentTimeMillis() + "==>TestThreadA start");
try{
System.out.println(System.currentTimeMillis() + "==>TestThreadA wait");
object.wait();
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + "==>TestThreadA end");
}
}
}
public static class TestThreadB extends Thread{
@Override
public void run(){
synchronized(object){
System.out.println(System.currentTimeMillis() + "==>TestThreadB start,notify someone Thread");
object.notify();
System.out.println(System.currentTimeMillis() + "==>TestThreadB end");
try{
Thread.sleep(2000);
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
Thread tA = new TestThreadA();
Thread tB = new TestThreadB();
tA.start();
tB.start();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
上述代码中,开启了两个线程 tA 和 tB。tA 执行了object.wait()方法。在执行wait()方法前,tA 先申请object的对象锁。因此,在执行object.wait()时,它是持有object的锁的。wait()方法执行后,tA 会进行等待,并释放object的锁。tB在执行notify()之前也会先获得object的对象锁。这里为了让测试效果明显,特意安排在notify()执行之后,让tB休眠2秒钟,这样做可以更明显地说明,notify()不会释放锁,并且tA在得到notify()通知后,需先重新尝试获得object的对象锁,获得后才能继续执行。
执行结果:
五、suspend和resume
suspend()方法和resume()方法都是Thread类中的方法,并且是一对相反的操作,suspend()方法使线程挂起,被挂起的线程必须等到resume()方法调用后才能继续执行。但它们也早已被标注为废弃方法,并不推荐使用。
之所以不推荐使用suspend()方法去挂起线程,是因为suspend()在使线程暂停的同时,并不会释放任何锁资源,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。意味着它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,仍然是Runnable,这也会严重影响我们对系统当前状态的判断。
六、volatile
volatile的原意为易变的、不稳定的,当使用volatile去声明一个变量时,这个变量在被修改后,应用程序范围内的所有线程都能够看到这个改动。
volatile可以极大保证操作的原子性,也能保证数据的可见性和有序性,但是并不能代替锁,它无法保证一些复合操作的原子性。比如volatile是无法保证i++的原子性操作的。
它只能确保一个线程修改了数据后,其他线程能够看到这个改动。但当两个线程同时修改某一个数据时,却依然会产生冲突。
public class Accounting implements Runnable{
static Accounting instance = new Accounting();
static volatile int i = 0;
public static void increase(){
i++;
}
@Override
public void run(){
for(int j=0;j<10000000;j++){
increase();
}
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(instance,"T1");
Thread t1 = new Thread(instance,"T2");
t1.start();
t2.start();
// 使主线程wait在t1线程上,直到t1执行完毕
t1.join();
// 使主线程wait在t2线程上,直到t2执行完毕
t2.join();
System.out.println(i);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
运行结果:
可以看到实际的结果为15333312,小于预期值20000000,这就是因为两个线程同时对 i 进行写入时,其中一个线程的结果会覆盖另外一个:线程T1和线程T2同时读取 i 为0,并各自计算得到 i = 1,并先后写入这个结果,虽然从整体看i++执行了两次,但实际却只增加了1。
七、join和yield
join() 方法会使调用的当前线程一直阻塞,直到目标线程执行完毕,也可以在调用时传递一个最大等待时间,如果超过给定的时间后目标线程还在执行,就不再阻塞。
public class JoinSample{
public volatile static int i = 0;
public static class AddThread extebds Thread{
@Override
public void run(){
for(i=0;i<10000000;i++);
}
}
public static void main(String[] args){
AddThread addThread = new AddThread();
addThread.start();
// 使主线程阻塞,一直到addThread线程执行完毕
addThread.join();
System.out.println(i);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
join()的本质是让调用线程wait()在当前线程对象实例上,当线程执行完成后,当前线程会在退出前调用notifyAll()通知所有的等待线程继续执行。
需要注意的是,不应在Thread对象实例上使用类似wait()或者notify()等方法,因为这很有可能会影响系统API的工作,或者被系统API所影响。
yield() 方法会使当前线程让出CPU,让出CPU后并不代表线程就结束了,而是再次进行CPU资源的争夺。
八、线程组
在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。在Java中,创建线程组通过一个ThreadGroup类:
public class ThreadGroupTest implements Runnable{
public static void main(String[] args){
// 创建一个线程组
ThreadGroup threadGroup = new ThreadGroup("myThreadGroup");
// 创建线程t1并将其放入threadGroup线程组
Thread t1 = new Thread(threadGroup, new ThreadGroupTest(),"T1");
// 创建线程t2并将其放入threadGroup线程组
Thread t2 = new Thread(threadGroup, new ThreadGroupTest(),"T2");
t1.start();
t2.start();
// 查看线程组中活动线程的数量,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确
System.out.println(threadGroup.activeCount());
// 查看线程组中线程的详情
threadGroup.list();
}
@Override
public void run(){
String groupAndName = Thread.currentThread().getThreadGroup().getName() + "-" + Thread.currentThread().getName();
while(true){
System.out.println("I am " + groupAndName);
try{
Thread.sleep(3000);
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
九、守护线程
守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程就是守护线程。与守护线程相对应的是用户线程,用户线程可以认为是系统的工作线程,负责处理系统的业务操作。当一个java应用内只有守护线程时,java虚拟机就会自动退出。前面说过守护线程是一种特殊的线程,因此要让线程变为守护线程,只需要调用它的setDaemon()方法,并传递true:
public class DaemonThreadSample{
public static class DaemonThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("DaemonThread is running");
try{
Thread.sleep(1000);
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread t = new DaemonThread();
// 将其设置为守护线程
t.setDaemon(true);
t.start();
// 主线程睡眠2秒,也就是说守护线程也只会存活2秒
Thread.sleep(2000);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
设置守护线程必须在线程start()之前设置,否则会抛出异常。但是程序和线程依然可以正常执行。只是被当做用户线程而已。
十、线程优先级
java中的线程可以设置优先级,使用1到10表示,优先级越高的线程在竞争资源时获取资源的概率会更大。只需通过调用线程实例的setPriority()方法。
Thread t = new MyThread();
// 将该线程的优先级设置为5
t.setPriority(5);
- 1
- 2
- 3
十一、线程安全与synchronized
使程序并发的一大好处就是可以提高程序的执行效率,但由于有可能多个线程同时修改一个数据,而将数据写坏,也就是产生了线程安全问题。因此Java中提供了许多机制来处理线程安全问题,其中最简单的就是synchronized关键字,它会对需要同步的代码加锁,使得每一次只能有一个线程进入同步块,线程间的执行因此变为同步执行。synchronized关键字可以有多种用法:
- 指定加锁对象:将指定的对象作为锁,进入同步代码前需获得该对象的锁;
- 直接用于修饰方法,相当于将当前实例作为锁,进入同步代码前需获得当前实例的锁;
- 直接用于修饰静态方法,相当于将当前类作为锁,进入同步代码前需获得当前类的锁。
我们可以使用synchronized改造volatile内容部分中的例子,使其变为线程安全的
public class Accounting implements Runnable{
static Accounting instance = new Accounting();
static volatile int i = 0;
public static void increase(){
i++;
}
@Override
public void run(){
for(int j=0;j<10000000;j++){
// 将instance作为锁,每次执行increase()时的线程需要获得instance对象的锁才行
synchronized(instance){
increase();
}
}
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(instance,"T1");
Thread t1 = new Thread(instance,"T2");
t1.start();
t2.start();
// 使主线程wait在t1线程上,直到t1执行完毕
t1.join();
// 使主线程wait在t2线程上,直到t2执行完毕
t2.join();
System.out.println(i);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
synchronized加锁的对象是实例变量时,应注意不能为不可变对象,例如String、Integer这种。因为这种不可变对象,在被修改后就会指向另一个对象,尽管变量还是原来的变量,但实际对象已经不是了,因此不能达到将同一个对象作为锁的目的。