Java多线程基础知识总结
线程的状态
NEW
线程被创建,但是还未调用start()方法就处于此状态。
RUNNABLE
Java线程将操作系统中READY和RUNNING合称为RUNNABLE。线程调用start()方法后处于READY(可运行)状态。处于可运行状态的线程获得了CPU时间片(timeslice)就处于RUNNING(运行中)状态。
BLOCKED
等待获取同步监视器(锁)的线程处于此状态。
WAITING
表示线程处于等待状态。例如,调用Object.wait()、Thread.join()、LockSupport.park()后线程就处于此状态。进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
TIMED_WAITING
超时等待状态。例如,调用Object.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil后线程就处于此状态。该状态不同于WAITING,它是可以在指定时间内自行返回的。
TERMINATED
终止状态,表示线程已经执行完毕。
创建线程的方式
- 继承Thread类,重写run方法;
- 实现Runnable接口,重写run方法;
- 通过Callable和Future创建,可以获得返回值。
这里主要讲下第三种方式。线程的执行体是Callable接口的call()方法,但是Callable接口不是Runnable接口的子接口,所以它不能作为Thread类的target。call()方法有返回值,用Future接口来代表该方法的返回值,Future接口有一个实现类FutureTask,并且FutureTask也实现了Runnable接口,所以FutureTask可以作为Thread类的target。示例代码如下:
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " i的值:" + i);
}
return i;
}
});
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " i的值:" + i);
if (i == 25) {
new Thread(task, "有返回值的线程").start();
Thread.yield();
}
}
try {
System.out.println("子线程返回值:" + task.get());
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
还有一种方法,通过线程池来提交Callable:
ExecutorService es = Executors.newSingleThreadExecutor();
Future<Integer> future = es.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 99; i++) {
System.out.println(Thread.currentThread().getName() + "->" + i);
}
return i;
}
});
Integer rsp;
try {
rsp = future.get();
System.out.println("返回值:" + rsp);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
es.shutdown();
线程安全问题
多个线程操作同一份资源时会出现线程安全问题
线程同步
synchronized关键字
- synchronized可以加在方法上,也可以加在代码块上,所以分为同步方法和同步代码块。
- 当synchronized加在非静态方法上时,所用的锁是this,也就是该对象本身;当synchronized加在静态方法上时,所用的锁是该类的class对象。因此,同一个对象的几个不同的非静态方法都用synchronized修饰,这几个方法不能同时运行,静态方法同理。
Lock接口和ReadWriteLock接口
死锁
同步锁的嵌套,互相等待对方释放锁,会导致死锁,示例代码如下:
package com.example.demo;
public class DeadLock {
protected static Object obj1 = new Object();
protected static Object obj2 = new Object();
public static void main(String[] args) {
Thread th1 = new Thread(new Task1(), "A");
Thread th2 = new Thread(new Task2(), "甲");
th1.start();
th2.start();
}
}
class Task1 implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 50; i++) {
synchronized(DeadLock.obj1) {
System.out.println(threadName + "获取到锁obj1");
synchronized(DeadLock.obj2) {
System.out.println(threadName + "获取到锁obj2");
}
}
}
}
}
class Task2 implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 50; i++) {
synchronized(DeadLock.obj2) {
System.out.println(threadName + "获取到锁obj2");
synchronized(DeadLock.obj1) {
System.out.println(threadName + "获取到锁obj1");
}
}
}
}
}
控制线程
开启:start
开启线程,让线程处在就绪的状态
休眠:sleep
让线程休眠一段时间,sleep不会释放锁
让步:yield
让处于运行状态的线程放弃CPU执行机会,并重新竞争CPU执行机会,也就是让线程回到就绪状态。完全可能的情况是:当某个线程调用了yield()方法放弃CPU执行机会之后,立马又竞争到了CPU执行机会
等待某个线程先执行:join
当前线程会等待调用join方法的线程执行完才继续执行
设置优先级:setPriority
优先级高的线程获得较多的执行机会,而优先级低的线程获得较少的执行机会。setPriority的参数是一个int类型,范围是1~10,值越大优先级越高
后台线程
有一种线程,它是在后台运行,它的任务是为其他的线程提供服务,这种线程被称为“后台线程”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。后台线程有一个特征:如果所有的前台线程都死亡,后台线程会自动死亡。通过setDaemon(true)方法可以将指定线程设置为后台线程。
线程通信
传统的线程通信
下面三个方法都是Object类的方法,必须由同步锁对象来调用。
wait
导致当前线程阻塞,直到其他线程调用该同步锁的notify()或notifyAll()来唤醒该线程,或者超时时间到了自动苏醒(带参数的wait方法)
notify
唤醒在此同步锁上等待的单个线程。如果有多个线程在此同步锁上等待,则会随机唤醒其中一个线程。注意,被唤醒的线程要去竞争获得同步锁才能被执行。
notifyAll
唤醒在此同步锁上等待的所有线程。同样,被唤醒的线程要去竞争获得同步锁才能被执行。
一个经典案例
假设现在有个杯子,服务员往杯子里倒水,顾客用这个杯子喝水,约定只有喝完了服务员才会往里面倒水。所以应该是倒一次水,喝完之后再倒一次,然后再喝完再倒,这样依次循环。。。
package com.example.demo;
public class WaitNotifyTest {
public static void main(String[] args) {
Cup cup = new OldCup();
Waiter waiterA = new Waiter(cup, "服务员A");
Waiter waiterB = new Waiter(cup, "服务员B");
Waiter waiterC = new Waiter(cup, "服务员C");
Customer me = new Customer(cup, "我");
Customer myWife = new Customer(cup, "我老婆");
waiterA.start();
waiterB.start();
waiterC.start();
me.start();
myWife.start();
}
}
interface Cup {
void input(String name);
void output(String name);
}
class OldCup implements Cup {
private boolean empty = true;
@Override
public synchronized void input(String name) {
//水杯有水,服务员等待
if (!empty) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//下面这个判断是针对多个服务员给同一个杯子倒水的情况
if (empty) {
System.out.println(name + "给客户倒了一杯水");
this.notifyAll();
this.empty = false;
}
}
@Override
public synchronized void output(String name) {
//水杯空了,等待服务员倒水
if (empty) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//下面这个判断是针对多个人喝同一杯水的情况
if (!empty) {
System.out.println(name + "喝了一杯水");
this.notifyAll();
this.empty = true;
}
}
}
class Waiter extends Thread {
private Cup cup;
public Waiter(Cup cup, String name) {
super(name);
this.cup = cup;
}
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
cup.input(this.getName());
}
}
}
class Customer extends Thread {
private Cup cup;
public Customer(Cup cup, String name) {
super(name);
this.cup = cup;
}
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
cup.output(this.getName());
}
}
}
上面代码的运行结果就是服务员倒一次水,客人喝一次水,倒一次,喝一次。。。
使用Condition控制线程通信
如果程序不使用synchronized关键字,而是使用Lock来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait、notify、notifyAll等方法了,这时我们可以使用Condition类,Condition的三个方法await、signal、signalAll分别对应传统的wait、notify、notifyAll等方法。我们可以基于Lock把上面的经典案例做一些修改,只需要增加一个NewCup类,实现Cup接口:
class NewCup implements Cup {
private final Lock lock = new ReentrantLock();
private final Condition con = lock.newCondition();
private boolean empty = true;
@Override
public void input(String name) {
lock.lock();
try {
//水杯有水,服务员等待
if (!empty) {
try {
con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//下面这个判断是针对多个服务员给同一个杯子倒水的情况
if (empty) {
System.out.println(name + "给客户倒了一杯水");
con.signalAll();
this.empty = false;
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
@Override
public void output(String name) {
lock.lock();
try {
//水杯空了,等待服务员倒水
if (empty) {
try {
con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//下面这个判断是针对多个人喝同一杯水的情况
if (!empty) {
System.out.println(name + "喝了一杯水");
con.signalAll();
this.empty = true;
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行上面示例,也可以得到倒一次水喝一次水效果。
volatile关键字
用于修饰一个变量。保证了不同线程对变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证原子性,因为就算线程的工作内存能够立即从主内存中load变量的新值,但是在往主内存写的时候还是会发生线程安全问题。volatile关键字现在用的少,主要是现在的计算机配置都比较好,主内存中的变量发生修改,会立马同步到各个线程的工作内存中。