JAVA程序天生就是多线程的。此言不虚。
1. 并发的优点和缺点
1.1. 优点
包含但不限于:
1.充分利用cpu的资源
2.减少用户响应的时间
3.程序模块化、异步化
在一些场景下很有用。如:批量文件IO、批量远程接口调用等。
1.2. 缺点
包含但不限于:
1.线程共享资源,存在冲突。
2.容易导致死锁。
举个栗子:
public static class T1 extends Thread {
@Override
public void run() {
synchronized (a){
//do sth.
synchronized (b){
//do sth.
}
}
}
}
public static class T2 extends Thread {
@Override
public void run() {
synchronized (b){
//do sth.
synchronized (a){
//do sth.
}
}
}
}
实际的情况要复杂的多。但原理类似。
3.启用太多的线程,就有搞垮机器的可能。
对线程进行管理要求额外的 CPU开销。线程数量多于内核核心数2倍时。执行效率会大幅降低。原因:1>Java->LWP->内核级线程。2>CPU时间片轮转(RR调度),会导致上下文切换。
- 容易引入线程安全问题
2. JAVA线程的几种实现方式:
Thread、Runnable、Callable
栗子:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author Ryan Lee
*/
public class InitThread {
public static class MyThread extends Thread {
@Override
public void run() {
/*协作。而不是简单粗暴*/
while (!isInterrupted()) {
System.out.println("My Thread");
}
}
}
private static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("My Runnable");
}
}
/*Callable接口,允许有返回值*/
private static class MyCallable implements Callable<String> {
@Override
public String call() {
System.out.println("My Callable");
return "CallResult";
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
//第一种方式
MyThread myThread = new MyThread();
myThread.start();
//第二种方式
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
Thread t = new Thread(myRunnable);
t.interrupt();
//第三种方式
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
其中Thread 实现 Runnable 接口。故同样可以声明一个继承Thread的类,来实现抽象方法run()。Callable允许有返回值。
Callable、Future、FutureTask之间的关系见下图:
image.png
3. 让Java线程停止工作的几种方式
3.1. 线程自然终止
自然执行完或抛出未处理异常
3.2. stop(),resume(),suspend()
不建议使用。stop()会导致线程不会正确释放资源。suspend()容易导致死锁。
3.3. interrupt()
java线程是协作式,而非抢占式。调用interrupt() 方法中断一个线程,并不是强行关闭这个线程。只是将线程的中断标志位置为true,线程是否中断,由线程本身决定。一般情况下,线程的run 函数中通过调用isInterrupted() 判定线程是否应继续执行。
/**
* @author Ryan Lee
*/
public class InterruptThread {
private static class RunnableDemo implements Runnable{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
//如果while的条件中不判断isInterrupted。即使主线程或其它线程调用本线程的interrupt()。本线程也不会终止。
while(1==1 && !Thread.currentThread().isInterrupted()) {
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
System.out.println("睡眠中被中断。截获中断异常后,中断标志位为"+Thread.currentThread().isInterrupted());
//此时中断标志位被复位(false)。如不显式调用中断,则线程会继续执行。
Thread.currentThread().interrupt();
}
System.out.println(threadName+" 正在运行");
}
System.out.println(threadName+"线程运行完成。中断标志位为" +Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
RunnableDemo runnableDemo = new RunnableDemo();
Thread interruptThread = new Thread(runnableDemo,"worker");
interruptThread.start();
Thread.sleep(2000);
interruptThread.interrupt();
}
}
3.4. 其它相关方法
- isInterrupted() 判定当前线程是否处于中断状态。
- interrupted() 判定当前线程是否处于中断状态,同时中断标志位改为false。
- 方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
4. 线程生命周期
image.png如图:线程只有5种状态。整个生命周期就是这几种状态的切换。
5. 线程操作的常用方法
5.1. run()和start()
- run方法只是普通的方法。调用一个Thread的run方法,只相当于调用一个普通对象的普通方法。不会生成新的线程。
- 调用了一个线程对象的start()后。Java才会将线程对象和操作系统中实际的线程进行映射,再来执行run方法。
/**
* @author Ryan Lee
*/
public class StartAndRun {
public static class ThreadDemo extends Thread {
@Override
public void run() {
int i = 90;
while (i > 0 && !isInterrupted()) {
try {
Thread.currentThread().sleep(100);
System.out.println("我是 " + Thread.currentThread().getName()
+ "现在做第" + i--+"件任务");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(e.getMessage());
interrupt();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.setName("子线程1");
//可以和用threadDemo.run()、threadDemo.start()。分别执行以下,看下区别
threadDemo.run();
//threadDemo.start();
Thread.sleep(1000);
threadDemo.interrupt();
}
}
- start()结果
我是 子线程1现在做第90件任务
我是 子线程1现在做第89件任务
我是 子线程1现在做第88件任务
我是 子线程1现在做第87件任务
我是 子线程1现在做第86件任务
我是 子线程1现在做第85件任务
我是 子线程1现在做第84件任务
我是 子线程1现在做第83件任务
我是 子线程1现在做第82件任务
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.xiangxue.ch1.StartAndRun$ThreadDemo.run(StartAndRun.java:17)
sleep interrupted
- run()结果
我是 main现在做第90件任务
我是 main现在做第89件任务
我是 main现在做第88件任务
我是 main现在做第87件任务
我是 main现在做第86件任务
我是 main现在做第85件任务
...
我是 main现在做第3件任务
我是 main现在做第2件任务
我是 main现在做第1件任务
5.2. stop()、resume()、suspend()、interrupt()
上文 让Java线程停止工作的几种方式 章节已讲到,这里不赘述。
5.3. wait()、notifyAll()、notify()
线程对象的wait()、notifyAll()、notify()方法均继承字Object类。
- wait()
线程从运行状态变为阻塞状态,直到被唤醒。wait操作会释放该线程说持有的锁。 - notifyAll()
通知在该对象/资源上wait的所有线程。
写个好玩的栗子:
/**
* @author Ryan Lee
*/
public class Flight {
private String origin;
private String destination;
private String location;
private static List<String> demoCityList = new LinkedList<>();
private static List<Integer> demoDisList =new LinkedList<>();
static {
demoCityList.add("厦门");
demoCityList.add("南昌");
demoCityList.add("济南");
demoCityList.add("天津");
demoDisList.add(3000);
demoDisList.add(2000);
demoDisList.add(1000);
demoDisList.add(0);
}
public synchronized void changeLocation(int i) throws InterruptedException {
location = demoCityList.get(i);
System.out.println("当前位置变化为:"+location);
Thread.currentThread().sleep(100);
notifyAll();
}
/**
* 乱写的计算剩余航程的DEMO方法
* @return
*/
private int getRemainDistance(){
if(demoCityList.contains(location)){
int i = demoCityList.indexOf(location);
return demoDisList.get(i);
}
else{
return 3000;
}
}
public Flight(String origin, String destination) {
this.location = origin;
this.origin = origin;
this.destination = destination;
}
public synchronized void pickUpWait() throws InterruptedException {
System.out.println("接机线程["+Thread.currentThread().getId()+"]判断是否等待");
Thread.currentThread().sleep(1000);
while(!Thread.currentThread().isInterrupted()&&getRemainDistance() > 1000) {
try {
//wait 会释放锁
System.out.println("接机线程["+Thread.currentThread().getId()
+"]在等待.");
wait();
System.out.println("接机线程["+Thread.currentThread().getId()
+"]被唤醒,重新判断剩余航程是否大于1000");
System.out.println("剩余航程:"+getRemainDistance());
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
System.out.println("剩余航程:"+getRemainDistance()+",接机人员已就位.");
}
public synchronized void ferryWait() throws InterruptedException {
System.out.println("摆渡车线程["+Thread.currentThread().getId()+"]判断是否等待");
Thread.currentThread().sleep(1000);
while(!Thread.currentThread().isInterrupted()&&!location.equals(this.destination)) {
try {
//wait 会释放锁
System.out.println("摆渡车线程["+Thread.currentThread().getId()
+"]在等待.");
wait();
System.out.println("摆渡车线程["+Thread.currentThread().getId()
+"]被唤醒,重新判断当前城市与目的地是否相同.");
System.out.println("当前城市:"+location+",目的城市:"+destination);
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
System.out.println("当前城市:"+this.location+",摆渡车已就位");
}
}
/**
* @author Ryan Lee
*/
public class TestWaitAndNotify {
private static Flight flight = new Flight("厦门", "天津");
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class PickUpThread extends Thread {
@Override
public void run() {
try {
flight.pickUpWait();
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class FerryThread extends Thread {
@Override
public void run() {
try {
flight.ferryWait();
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public static void main(String[] args) throws InterruptedException {
new FerryThread().start();
new PickUpThread().start();
Thread.sleep(10000);
flight.changeLocation(0);//快递地点变化
Thread.sleep(10000);
flight.changeLocation(1);//快递地点变化
Thread.sleep(10000);
flight.changeLocation(2);//快递地点变化
Thread.sleep(10000);
flight.changeLocation(3);//快递地点变化
}
}
执行结果
摆渡车线程[19]判断是否等待
摆渡车线程[19]在等待.
接机线程[20]判断是否等待
接机线程[20]在等待.
当前位置变化为:厦门
接机线程[20]被唤醒,重新判断剩余航程是否大于1000
剩余航程:3000
接机线程[20]在等待.
摆渡车线程[19]被唤醒,重新判断当前城市与目的地是否相同.
当前城市:厦门,目的城市:天津
摆渡车线程[19]在等待.
当前位置变化为:南昌
摆渡车线程[19]被唤醒,重新判断当前城市与目的地是否相同.
当前城市:南昌,目的城市:天津
摆渡车线程[19]在等待.
接机线程[20]被唤醒,重新判断剩余航程是否大于1000
剩余航程:2000
接机线程[20]在等待.
当前位置变化为:济南
接机线程[20]被唤醒,重新判断剩余航程是否大于1000
剩余航程:1000
剩余航程:1000,接机人员已就位.
摆渡车线程[19]被唤醒,重新判断当前城市与目的地是否相同.
当前城市:济南,目的城市:天津
摆渡车线程[19]在等待.
当前位置变化为:天津
摆渡车线程[19]被唤醒,重新判断当前城市与目的地是否相同.
当前城市:天津,目的城市:天津
当前城市:天津,摆渡车已就位
上面的例子只是单机多线程的简单示例。纯属虚构。
- notify()
随机通知一个等待资源的线程。容易导致结果的不确定性。不建议使用。
拿上面的实例说事儿:两个线程同时争夺Flight类型对象flight的锁。但判断是否继续执行的条件不同。接机线程:公里数小于等于1000;摆渡车线程:航班当前所在城市为目的城市。
如果随机通知一个,容易导致获得flight锁的线程不满足继续执行的条件;而满足继续执行条件的线程又没有被唤醒。
例如:当前位置变换为济南时,距目的地公里数变成1000。随机通知到摆渡车线程。摆渡车线程被唤醒,却不满足执行条件。然后摆渡车线程执行wait方法,进入阻塞状态。而符合继续执行条件的接机线程却错过了被唤醒的机会,继续阻塞。而下一次notify也不一定会通知到符合继续执行条件的接机线程。
5.4. sleep()
线程从运行状态变为阻塞状态,直到睡眠时间结束。sleep操作不会释放线程所持有的锁。
/**
* @author Ryan Lee
*/
public class TestSleepLock {
private Object lock = new Object();
private class ThreadSleep extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" 即将尝试获取锁");
try {
synchronized(lock) {
System.out.println(threadName+"拿到了锁");
System.out.println(threadName+"执行完成");
//可以尝试移到synchronized块外,看看执行效果
Thread.sleep(5000);
}
} catch (InterruptedException e) {
//此处应该写interrupt()。只是主线程中没有调用中断方法。故写不写效果一样。
}
}
}
private class ThreadNotSleep extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" 即将尝试获取锁");
long start = +System.currentTimeMillis();
synchronized(lock) {
long end = +System.currentTimeMillis();
System.out.println(threadName+"拿到了锁,耗时"+(end-start)+"ms");
System.out.println(threadName+"执行完成");
}
}
}
public static void main(String[] args) {
TestSleepLock sleepTest = new TestSleepLock();
Thread threadA = sleepTest.new ThreadSleep();
threadA.setName("睡眠的线程");
Thread threadB = sleepTest.new ThreadNotSleep();
threadB.setName("不睡的线程");
threadA.start();
//为了确保threadA拿到锁
try {
Thread.sleep(100);
System.out.println("主线程睡醒了");
} catch (InterruptedException e) {
e.printStackTrace();
}
threadB.start();
}
}
5.5. join()
插队,确保执行顺序。如在thread1中执行thread0.join(),表示thread0需要再thread1执行前执行。栗子如下:
/**
* @author Ryan Lee
*/
public class TestJoin {
static class JumpQueue implements Runnable {
private Thread thread;//用来插队的线程
public JumpQueue(Thread thread) {
this.thread = thread;
}
public void run() {
try {
System.out.println(thread.getName()+"插队在" +Thread.currentThread().getName()+"前面");
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 执行完成.");
}
}
public static void main(String[] args) throws InterruptedException {
Thread previous = Thread.currentThread();//现在是主线程
for (int i = 0; i < 10; i++) {
//i=0,previous 是主线程,i=1;previous是i=0这个线程
Thread thread =
new Thread(new JumpQueue(previous), "排队线程"+i);
thread.start();
previous = thread;
}
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " 执行完成.");
}
}
执行结果
main插队在排队线程0前面
排队线程2插队在排队线程3前面
排队线程1插队在排队线程2前面
排队线程0插队在排队线程1前面
排队线程4插队在排队线程5前面
排队线程3插队在排队线程4前面
排队线程5插队在排队线程6前面
排队线程6插队在排队线程7前面
排队线程7插队在排队线程8前面
排队线程8插队在排队线程9前面
main 执行完成.
排队线程0 执行完成.
排队线程1 执行完成.
排队线程2 执行完成.
排队线程3 执行完成.
排队线程4 执行完成.
排队线程5 执行完成.
排队线程6 执行完成.
排队线程7 执行完成.
排队线程8 执行完成.
排队线程9 执行完成.
5.6. yield()
让出cpu的执行权。线程从运行状态变为可运行状态。但是下个时间片,该线程依然有可能被再次选中运行。具体看操作系统的线程调度结果。
其它
- 线程的优先级:取值为1~10,缺省为5,但线程的优先级不可靠,不建议作为线程开发时候的手段
- 守护线程:和主线程共死,finally不能保证一定执行
网友评论