Java多线程
1.线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
一个线程在它的完整生命周期中通常要经历以下五种状态:
- 新建:当一个
Thread
类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 - 就绪:处于新建状态的线程被
start()
后,将进入线程队列等待CPU时间片,此时他已经具备了运行条件,只是没分配到CPU资源 - 运行:当就绪状态的线程被调度并获得CPU资源时,便进入运行状态,
run()
方法定义了线程的操作和功能 - 阻塞:在某种特定情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
2.线程的同步
背景
线程之间共享数据,多个线程执行的不确定性引起执行效果的不稳定
解决方法:
在Java中,通过同步机制,解决线程的安全问题
方式一:同步代码块
/*
synchronized(同步监视器){
//需要被同步的代码
}
说明:1.操作共享数据的代码,即为需要被同步的代码
2.同步监视器:锁。任何一个类的对象,都可以充当锁
要求:多个线程必须要共用同一把锁
*/
同步代码块解决重写Runnable
多线程安全问题
/*
* 1.问题:多窗口卖票的过程中,出现了错票和重票---》》线程的安全问题
* 2.原因:某个线程操纵车票的过程中,尚未操作完成时,其他线程也参与进来,操作车票
* 3.解决方法:加锁;某个线程a在操作车票时,其他线程不能参与进来,知道线程a操作结束为止
* 4.Java中:通过同步机制,解决线程安全问题
*/
public class MultiWindowTest1 {
public static void main(String[] args) {
NormClass n=new NormClass();
Thread t1=new Thread(n);
Thread t2=new Thread(n);
Thread t3=new Thread(n);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class NormClass implements Runnable{
private int ticket=1000;
Object obj=new Object(); //同步监视器
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "\t 票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
同步代码块解决继承Thread
类多线程安全问题
public class MultiWindowTest {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class MyThread extends Thread{
private static int ticket=1000;
private static Object obj=new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
System.out.println("窗口为:" + this.getName() + "\t 票数号码为:" + ticket);
ticket--;
} else break;
}
}
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步
同步方法解决重写Runnable
多线程安全问题
public class WindowTest3 {
public static void main(String[] args) {
Mthread h=new Mthread();
Thread t1=new Thread(h);
Thread t2=new Thread(h);
Thread t3=new Thread(h);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Mthread implements Runnable{
private int ticket=1000;
@Override
public void run() {
while (true){
show();
if(ticket==0) break;
}
}
private synchronized void show() { //默认当前的同步监视器为this
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": 当前票号为" + ticket);
ticket--;
}
}
}
同步方法解决继承Thread
类多线程安全问题
public class WindowTest4 {
public static void main(String[] args) {
MThread4 t1=new MThread4();
MThread4 t2=new MThread4();
MThread4 t3=new MThread4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class MThread4 extends Thread{
private static int ticket=1000;
@Override
public void run() {
while (true){
show();
if(ticket==0) break;
}
}
//注意:1.此时同步的方法需添加static,否则会报错,应为多个线程之间要共用同一把锁
// 2.静态方法无法调用this,此时充当同步监视器的是当前类
private static synchronized void show(){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+" :当前票号"+ticket);
ticket--;
}
}
}
总结:
- 同步方法的方式仍然涉及到同步监视器,只是不需要我们显式地声明
- 非静态的同步方法,同步监视器:this
- 静态的同步方法,同步监视器:当前类本身
应用:
线程同步解决懒汉式单例设计的安全问题
同步代码块方式:
class SingleTest{
private static SingleTest instance=null;
private SingleTest(){}
public static SingleTest getInstance(){
//方式1:效率较低
// synchronized (SingleTest.class) {
// if (instance == null) {
// instance = new SingleTest();
// }
// return instance;
// }
//方式2:效率较高
synchronized (SingleTest.class){
if(instance==null) {
synchronized (SingleTest.class) {
if (instance == null) {
instance = new SingleTest();
}
}
}
return instance;
}
}
}
同步方法方式:
class SingleTest{
private static SingleTest instance=null;
private SingleTest(){}
public static synchronized SingleTest getInstance(){
if(instance==null){
instance=new SingleTest();
}
return instance;
}
}
3.线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
线程死锁示例:
public class DeadLockTest1 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
s1.append('a');
s2.append('1');
synchronized (s2) {
s1.append('b');
s2.append('2');
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
s1.append('c');
s2.append('3');
synchronized (s1) {
s1.append('d');
s2.append('4');
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
如何解决死锁问题:
- 专门的算法,原则避开死锁的可能性
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
4.Lock
锁解决线程安全问题
JDK5.0
开始,Java
提供了更强大的线程同步机制:显示定义同步锁实现同步。同步锁使用Lock
对象充当。java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock
对象加锁,线程访问共享资源之前应先获得Lock
对象ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显示加锁、释放锁。
使用格式
实例
import java.util.concurrent.locks.ReentrantLock;
public class Exe2 {
public static void main(String[] args) {
Window w=new Window();
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private int ticket=100;
//显示定义同步锁对象实现线程同步
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
lock.lock(); //加锁
try{
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
}
else
break;
}
finally{
lock.unlock(); //解锁
}
}
}
}
syschronized
机制与lock
机制的异同点
- 相同点:
- 两者都可以实现线程同步,解决线程安全问题
- 不同点
synchronized
机制执行完相应的代码逻辑后,会自动释放同步监视器lock
机制需手动实现加锁(lock())
和解锁(unlock())
5.线程的通信
相关的API
wait()
:一旦执行此方法,当前线程会进入阻塞状态,并释放同步监视器notify()
:一旦执行此方法,就会唤起被wait
的一个线程,如果有多个线程被wait
,则唤醒优先级高的那一个notifyAll()
:一旦执行此方法,就会唤醒所有被wait
的进程
注意:
- 上面的三个方法只能使用在同步代码块或同步方法中
lock
机制使用其他的方法进行进程通信
- 上面三个方法的调用者必须是同步代码块或同步方法中的同步监视器
- 上面三个方法是定义在
java.lang.Object
中
实例
/*
* 线程之间通信:
* 实现两个线程交替打印1~100数字
*/
public class Exe4 {
public static void main(String[] args) {
ThreadMy w=new ThreadMy();
Thread t1=new Thread(w);
Thread t2=new Thread(w);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class ThreadMy implements Runnable{
private int number=1;
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
synchronized (this) { //实现线程同步
notify(); //唤起当前阻塞的线程
//两个及以上阻塞的线程全部唤起使用 notifyAll
if(number<=100){
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait(); //当前线程阻塞,同时会释放同步锁
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
else
break;
}
}
}
}
常见面试题
1. sleep()与wait()的异同
-
相同点:
- 执行对应方法,都可以使当前线程进入阻塞状态
-
不同点:
-
两个方法申明的位置不同:
Thread()
类中申明sleep()
,Object
类中申明wait()
-
调用的范围要求不同:
sleep()
可以在任何场景下调用wait()
必须在同步代码块或同步方法中调用
-
sleep()
不会释放同步监视器,wait()
会释放同步监视器
-
6. JDK5.0
新增线程创建方式
6.1 实现Callable
接口
-
与使用
Runnable
相比,其功能更强大- 相比于
run()
方法,可以有返回值 - 方法可以抛出异常
- 支持泛型的返回值
- 需要借助
FutureTask
类,比如获取返回结果
- 相比于
-
Future
接口- 可以对具体
Runnable
、Callable
任务的执行结果进行取消、查询是否完成、获取结果等。 FutureTask
是Future
接口的唯一实现类FutureTask
同时实现了Runnable
、Future
接口。它既可以作为Runnable
被线程执行,又可以作为Future
得到Callable
的返回值。
- 可以对具体
实例
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
* 实现Callable接口方式实现多线程
*/
public class Exe6 {
public static void main(String[] args) {
//3.实例化Callable实现类对象
ThreadNew t=new ThreadNew();
//借助FutureTask获取返回值
//4.将Callable实现类对象作为参数实例化FutureTask实现类对象
FutureTask f=new FutureTask(t);//Callable实现类对象作为参数
//线程启动
//5.FutureTask实现类对象作为参数实例化Thread对象,调用start()启动线程
new Thread(f).start();
try {
//6. 如果要获取返回值,通过FutureTask类的get()获取
Object sum = f.get(); //通过get()获取线程返回值
System.out.println("总和:"+sum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//1.创建Callable接口的实现类
class ThreadNew implements Callable{
private int sum=0;
//2.重写call(),线程要执行的操作为方法的方法体
@Override
public Object call() throws Exception {
for(int i=0;i<100;i++){
if(i%2==0) sum+=i;
}
return sum; //自动装箱为Integer
}
}
6.2 使用线程池
实例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
* 使用线程池的方式来实现多线程
*/
public class Exe7 {
public static void main(String[] args) {
//1.提供指定数量的线程池
ExecutorService service=Executors.newFixedThreadPool(10);
//2.执行指定类操作,需提供一个实现Runnable或Callable接口实现类对象
service.execute(new Thread1()); //执行线程
service.execute(new Thread2());
// service.submit() //callable接口使用submit()
//3.关闭连接池
service.shutdown(); //关闭线程池
}
}
class Thread1 implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class Thread2 implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2!=0)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}