一. 概述
使用的线程的目的有如下几点:
- 异步。所谓异步,字义上来讲就是同时做多个不同的事。
例如,你正在和恋人聊QQ,而此时你正在发送一个文件,如果收发消息和上传文件在同一个线程,那么当你发送文件开始,你便需要等待文件上传完成后,才能发送下一条消息,如果再加上文件太大、网速很差等等因素,就可能导致你和你恋人在几个小时内只说了一句话,那你们之间可能就GG了。
- 并发。所谓并发,字义上来讲就是同时做多个相同的事。
例如,双11上淘宝买东西,在同一时间内,阿里的服务器收到一多个购买的请求,如果处理这些请求的方式是处理完一条再处理下一条的话,那么你买个衣服坑你就要等到地老天荒了。
虽然我在这里将线程的目的分成了两种,但它们的本质是一样:在同一时间做多个不同的事。值得注意的是,如果你电脑的CPU是单核单线程的话,这个“同一时间”是有事件差的,可能CPU这一毫秒在执行线程A,下一毫秒在执行线程B,但完全同一时间同时执行线程A和线程B是不可能的。
二. 如何创建线程
实现线程有两种方式,涉及到的类有两个,分别是:java.lang.Thread和java.lang.Runnable。创建并启动线程的方式如下:
Thread thread = new Thread();//创建线程实例
thread.start();//启动线程
但是这仅仅是创建了一条新的线程并启动了它,该线程并不会执行任何逻辑,那么我们怎么让它执行相关逻辑呢?
方式1:继承Thread类,重写run方法。
public class DemoThread extends Thread {
public void run() {
//TODO 线程中需要执行的相关逻辑
}
public static void main(String[] args) {
Thread thread = new DemoThread();//创建线程实例
thread.start();//启动线程
}
}
方式二:实现Runnable接口,并在Thread构造方法中传入实现的Runnable实例。
public class DemoRunnable implements Runnable {
public void run() {
//TODO 线程中需要执行的相关逻辑
}
public static void main(String[] args) {
Runnable runnable = new DemoRunnable();//创建一个实现了Runnable接口的类的实例
Thread thread = new Thread(runnable);//创建线程实例,并在构造方法中传入实现了Runnable类的实例
thread.start();//启动线程
}
}
注: 这两种方法都能让线程执行我们需要执行的逻辑代码。当你调用start()函数启动线程后,程序会在该线程中调用Thread类自己的run()方法,如果你在创建线程时传入了Runnable的实例,那么在Thread类的run()方法中,会调用Runnable的run()方法。
特别需要注意的是,new Thread()只是创建了Thread类的一个实例,此时并没有创建出一条线程。而启动线程是调用start()方法而不是run()方法:直接调用run()方法时你只是调用的一个普通方法去执行相关逻辑代码,逻辑依然执行再你调用run()方法的线程中;而调用start()方法,jvm才会创建一条新线程,而此时run()才会在该线程中自动被回调执行。
三、线程同步
什么是同步?
同步是某个任务在同一时间只能由一个线程在执行,等这个线程执行完成后,下一个线程才能执行。那么既然线程的目的是为了异步,那么又为什么需要同步呢?这主要是由于数据安全造成的。多个线程在同时操作一个数据,当线程A还没没来得急使用该数据时,线程B就改了该数据的状态或值,这是就可能导致结果的错误,甚至是程序运行的异常。就像由此你和朋友都很饿,然后看到一个苹果,你正准备吃,然后你朋友直接给你抢来吃的还剩核,然后......例子可能不精确,见谅。
实现线程同步主要要使用两个点:锁和synchronized关键字。
1.锁。
锁是java中一种机制,分为对象锁和类锁。每个对象/类都有一个单一的锁(需要注意的是,一个类可以有多个对象,每个对象的锁也都是独立且单一的,互不干扰)。当一个线程获取到某个对象/类的锁后,除非该锁被释放,否则其他线程是不能获取到该对象/类的锁,而此时如果其他线程要获取该对象的锁,就只能等待。而上面所说的某个任务,便是获取到同一个锁的代码块,他可能里面的逻辑并不相同,但是获取的锁是相同的。
2、synchronized关键字用于需要同步的逻辑中,实现方式分为:同步方法和同步块。synchronized的目的就是获取某个对象/类的锁。分为:
同步块。其中的object参数便是你要获取的锁的对象。
synchronized (object) {
//TODO 这里是同步块中需要执行的逻辑
}
对象同步方法。该synchronized获取到的锁是类A时候化后的对象的锁。
public class A {
public synchronized void syncMethod() {
//TODO 这里是同步方法中需要执行的逻辑
}
}
类同步方法,即静态同步方法。该synchronized获取到的锁是类A的锁。
public class A {
public static synchronized void staicSyncMethod() {
}
}
同步的例子
public class ThreadSyncDemo {
private static Object locker = new Object();//需要获取锁的对象
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker) {
for (int i = 0; i < 1000; ++i) {
System.out.println(i);
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker) {
for (int i = 0; i < 1000; ++i) {
System.out.println("aaa");
}
}
}
});
t1.start();
t2.start();
}
}
执行这段代码时,你先将synchronized块去掉,保留里面的逻辑,然后执行,此时你会返现控制台输出中,数字和字母是交叉出现的,说明是异步执行的。然后加上同步快,再执行,你会发现此时数字先打印完后才打印的字母(或者字母先打印完后才打印的字母),说明是同步执行的。对象同步方法和静态同步方法原理的列子这里就不再举出,只要知道到底是获取到那个对象或类的锁,其他都是类似的。
死锁
死锁,顾名思义就是锁死了,执行不下去了。若有两个线程A和B,若线程A在执行时获取到对象X的锁,在同步块中又获取对象Y的锁,而此时线程B在执行时获取到对象Y的锁,而B的同步块中又在获取X的锁。再某种比较的极端情况下,A持有X的锁,B持有Y的锁,A执行到获取Y的锁时B未执行完,A阻塞等待,然后B又获取X的锁,而此时A还在阻塞等待B持有的Y的锁,未释放X的锁,导致B也阻塞等待。A和B都在阻塞等待,然后就没有然后了~~~~
所以避免死锁的其中之一便是尽量不要交叉获取锁。当然这不是唯一导致死锁的可能。
下面是一个死锁的列子:
public class ThreadSyncDemo {
private static Object locker = new Object();
public static void main(String[] args) {
final Thread t1 = new Thread() {
@Override
@Deprecated
public void run() {
synchronized (locker) {//获取ThreadSyncDemo的类锁
for (int i = 0; i < 10000; ++i) {
System.out.println(i);
if (i == 1000) {
this.suspend();//挂起该线程,此方法暂停线程执行,但不会释放锁
}
}
}
}
};
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker) {//获取ThreadSyncDemo的类锁
for (int i = 0; i < 10000; ++i) {
System.out.println("aaa");
}
}
}
});
t1.start();
try{
Thread.sleep(50)//暂停下,保证他t1先执行。
}catch(Exception e){}
t2.start();
}
}
上面例子中,线程t1和线程t2的同步块都是获取的ThreadSyncDemo.class的锁,在t1同步块中,当i=1000时,暂停了t1线程的执行,此时t1的同步块未释放锁,而t2一直在等待t1释放锁,如果t1线程不继续执行,则t2也执行不了。
四. Thread类的相关方法
wait()与notify()/notifyAll()方法
实际上,wait()、notify()、notifyAll()这三个方法并不是Thread类的专有方法,而是Object的方法,也就是说,每个对象都存在这三个方法。
需要注意的是,这三个方法都只能在同步块/同步方法中执行,其他地方执行时没有意义的。而且需要同步块中获取到的锁的对象来调用才有效果。
wait()顾名思义让同步块暂停执行并等待,此时该同步块会让出获取到的锁,让其他线程执行获取同一把锁的同步块。而在其他线程执行完后,调用notify()/notifyAll()方法,之前等待的的同步块就会继续执行。但是,如果调用了notify()/notifyAll()之后,后面有长时间任务二导致锁未被释放,等待中的同步块也需要等锁被释放后才会继续往下执行。如果同一个锁有多个地方等待,就需要使用notifyAll()来全部唤醒,使他们重新争夺锁的行列中,谁先获取到锁就谁先执行。注意如果等待的是多个,nofity()和nofityAll()后最先获取的锁的是那个,由jvm决定。
wait()方法有两个个重载方法,wait(long timeout)和wait(long timeout, int nanos),其中timeout是等待时间,如果timeout=0,则表示一直等待知道nofity()/nofityAll()被调用切获取到锁后继续执行,timeout>0则表示等待多少时间后,只要获取到锁就继续执行。至于nanos,表示纳秒,值在0-999999之间,为了更好的控制时间。
public class ThreadSyncDemo {
private static Object locker_1 = new Object();
public static void main(String[] args) {
final Thread t1 = new Thread() {
@Override
@Deprecated
public void run() {
synchronized (locker_1) {
for (int i = 0; i < 1000; ++i) {
System.out.println(i);
if (i == 100) {
try {
locker_1.wait();//当i=100是,执行wait()方法,释放锁,进入等待状态
} catch (Exception e) {
}
}
}
}
}
};
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (locker_1) {
for (int i = 0; i < 100; ++i) {
System.out.println("aaa");
}
locker_1.notify();//当执行完后,通知等待的线程继续执行。
}
}
});
t1.start();
try{
Thread.sleep(50) //保证t1先执行
}catch(Exception e)
t2.start();
}
}
上例,控制台先会打印0-100,当线程t1中i=100时,调用locker_1.wait()使其释放锁并进入等待状态,此时线程t2获取到锁,控制台打印100个aaa,然后调用locker_1.notify()后,线程t1会继续在控制台打印101-999。
interrupt(),interrupted(),isInterrupt()方法
interrupt()方法看起来是中断线程,但实际上,当你调用interrupt()方法后,你发现线程该干嘛还是再干嘛,除非你的线程中存再调用sleep()、wait()等方法的时候,此时会抛出InterruptedException异常,以供手动处理线程停止。isInterrupt()返回线程是否是中断状态。interrupted()方法是类方法。从Thread类的源码看,isInterrupt()和interrupted()都会调用以下方法:
private native boolean isInterrupted(boolean ClearInterrupted);
不同是isInterrupt()传入的ClearInterrupted=false,
interrupted()方法传入的ClearInterrupted=true,当ClearInterrupted=true时,线程的中断状态会被清除,也就是说此时isInterrupt()方法的返回值是false。
suspend()与resume()方法
已废弃的方法,用于暂停和重新开始执行线程。和wait()不同的是,suspend()是通过线程的实例调用的,而不是锁对象,调用也不需要在同步块中调用,而且suspend()方法调用后并不会释放锁。列子:
public class ThreaSuspendResumeDemo {
public static void main(String[] args) {
final Timer timer = new Timer();
final Thread t = new Thread() {
@Deprecated
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
if (i == 1000) {
this.suspend();//暂停线程
}
}
timer.cancel();
}
};
t.start();
timer.schedule(new TimerTask() {
@Override
@Deprecated
public void run() {
t.resume();//3秒后将线程唤醒
}
}, 3000);
}
}
stop()方法
废弃的方法。暴力终止线程,调用此方法后,线程中未执行的语句将不会再执行。但是isAlive()方法和isInterrupt方法的返回值依然是false,所以,暴力如此,想想都可怕,谨慎使用。
public class ThreadStopDemo {
public static void main(String[] args) {
final Thread t = new Thread() {
@Deprecated
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
if (i == 1000) {
this.stop();//i=1000时就停止了,控制台只会打印0-1000。
}
}
System.out.println("Is this thread alive?" + this.isAlive());//线程已经终止,这条语句是不会执行的
}
};
t.start();
final Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
@Deprecated
public void run() {
System.out.println("Is this thread alive?" + t.isAlive());//false
System.out.println("Is this thread interrupt?" + t.interrupted());//false
}
}, 3000);
}
}
destroy()方法
额,为什么要讲这个方法呢?因为我以为会和stop()方法一样的丧心病狂,但是我错了。从Thread.destroy()方法的源码来看,结果让人发呆流鼻涕。源码如下:
/**
* Throws {@link NoSuchMethodError}.
*
* @deprecated This method was originally designed to destroy this
* thread without any cleanup. Any monitors it held would have
* remained locked. However, the method was never implemented.
* If if were to be implemented, it would be deadlock-prone in
* much the manner of {@link #suspend}. If the target thread held
* a lock protecting a critical system resource when it was
* destroyed, no thread could ever access this resource again.
* If another thread ever attempted to lock this resource, deadlock
* would result. Such deadlocks typically manifest themselves as
* "frozen" processes. For more information, see
* <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">
* Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
* @throws NoSuchMethodError always
*/
@Deprecated
public void destroy() {
throw new NoSuchMethodError();
}
是不是亮瞎了钛合金狗眼!!!!!
五.守护线程
线程分为用户(User)线程和守护(Daemon)线程,守护(Daemon)线程的实现就是线程在调用start()方法前,先调用setDaemon(true)方法。区别是,普通用户(User)线程,只要线程还在执行,那么程序就永远不会退出;而守护(Daemon)线程只要程序主线程执行完后,守护(Daemon)线程也就被终止。守护(Daemon)线程常作为辅佐的的作用。
以上便是此次线程学习的第一部分总结,如有意见或建议欢迎提出,相互探讨才能共同成长与进步。后面讲继续研究线程池和线程调度。