java 多线程
多线程
多线程,可以理解为应用到CPU的一条执行通道,当然CPU能否执行,也就取决于CPU的调度方案了
Thread 类
继承 Thread 类是玩多线程的一种方式
设置线程类继承Thread 并重写方法run
在主线程中可以创建该线程对象,并执行start方法启动线程,这时候JVM会帮你调用run方法
注意,多次启动线程是非法的!
线程名
主线程的线程名为main,其他线程默认为Thread-x
x 为标识数值
获取线程名的方法为Thread 类的 getName()方法
在JVM开启主线程也是线程,也是Thread对象,Thread类中有个静态方法
static Thread currentThread()
用以返回当前正在运行的线程,通过这个线程对象来获取主线程的线程名字
setName 设置线程名称
Thread的构造器也可以设置线程名称
线程休眠
Thread.sleep(sec) 以毫秒为单位休眠
这个休眠会产生异常,这个异常一般是线程在特殊情况下被唤醒导致
Runnable接口
创建线程对象
实现Runnable接口也是执行多线程程序的一种方式
只需要重写一个方法run即可
public class SubRun implements Runnable {
public void run() {
// todo
}
}
开启线程
在Thread构造方法构造器可以传递一个Runnable实现类的参数,创建线程对象,开启线程通道
同样调用线程对象的方法start启动线程即可
public static void main(String args[] ) {
SubRun sr = new SubRun();
Thread t = new Thread(sr);
t.start();
}
实现类的好处
避免了继承的弊端,接口可以多实现
实现接口的方式更加符合面向对象对象的方式,继承Thread类,将线程对象和线程任务耦合一起,一旦创建线程对象,就有了线程任务,难以分开
实现类的好处就是分离再封装对象,达到线程对象和线程任务的解耦
实现接口的方式,还可以方便资源共享
使用匿名内部类
继承类重写的方式写法
new Thread() {
public void run() {
System.out.println("....");
}
}.start();
实现接口重写方式写法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("....");
}
}).start();
线程状态图
线程Thread类中有个嵌套类 Status 里边记录了很多的状态
-
NEW: 线程对象已经创建,new 出来后就处于这张状态
-
RUNNABLE 方法start() 会进入到此状态 运行状态
-
BLOCKED 受阻塞状态,执行start()后未必到RUNNABLE,可能到达此状态,比如受同步锁限制或CPU限制,这是程序无法控制的
即使是RUNNABLE状态,中间由于CPU资源抢夺,可能又会回到受阻塞状态,这也是程序无法控制的
-
TIMED_WAITTING 休眠状态,等待另一个进程执行,唤醒后未必能马上运行,即受阻塞
-
WAIT 调用wait()方法,永远等待另一个线程执行完成,等待另一个线程唤醒,同样唤醒后未必马上执行,即受阻塞
-
TERMINATED 死亡状态,线程已执行完毕
线程池
线程池其实就是一个能容纳多个线程的容器,其中的线程可以反复使用,避免了创建线程和线程死亡带来的资源消耗
这样的话,线程池里的线程状态就更多地在有用的循环中跑
原理
ArrayList<Thread> threads = ...;
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
Thread t = threads.remove(0);
t.start();
threads.add(t);
内置线程池 Executors
JDK 1.5 后java内置了线程池技术,直接使用即可
线程池都是通过线程池工厂创建,再调用线程池的方法获取的线程
java.util.concurrent.Executors
这个类中的方法都是静态方法
ExecutorsServices newFiexedThreadPool(int nThreads)
返回的是接口实现类,其实就是线程池对象
submit(Runnable r)
接口实现类对象,调用方法submit(Runnable r),提交线程执行任务
调用后,只要线程池存在,程序就不会停下来
shutdown()
销毁线程池,一般不用
实现callable
在使用Runnable方法重写run,有一个不好之处在于run没有返回值,而且也不能抛出异常
这时候实现callable就来解决这个问题
Callable 类的call方法原型,返回值是泛型,重写的子类异常可抛可不抛
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
创建Callable实现类
import java.util.concurrent.Callable;
public class SubCall implements Callable<String> {
@Override
public String call() throws Exception {
return "哈哈哈";
}
}
调用线程
- 使用工程类Executors静态方法newFiexedThreadPool创建线程池对象
- 得到的ExecutorsServices接口实现类,调用submit(Callable c)方法,该方法将返回Future的实现类
- 利用Future接口的方法get拿到返回值
ExecutorService es = Executors.newFixedThreadPool(2);
Future<String> f = es.submit(new SubCall());
try {
String s = f.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
线程共享安全问题
问题的来源
线程安全问题,通常是多个线程共同操作同一个数据
参考如下线程对象
public class SubRun implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}
}
}
}
假设创建了三个线程,方法run将进栈3次,这3个方法使用的成员变量是同一个,都在堆内存中。
同步代码块
synchronized(任意对象) {
// 共享数据的操作
}
加上了同步操作后,运行速度就变慢了
同步原理
同步的依赖: 同步对象,同步锁,对象监视器
没有同步锁的线程不能执行,只能等
每个线程遇到同步代码块后,都会进行判断对象同步锁还是否存在,有则“拿走”,没有则不能执行,“拿走”锁的线程执行完后,会归还同步锁给下一个线程使用
这样线程每次判断锁,获取锁,释放锁,就导致线程安全速度变慢的原理了
同步方法
一般同步代码块放在一个方法中,这样可以在方法的声明上添加synchronized关键字,从而实现同步方法
这样写的好处在于代码的简洁分离,不需要显示声明额外的同步锁对象
public synchronized void sellTicket() {
if (ticket > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}
}
虽然没有显示声明,但还是有锁的,这个锁对象就是this
本类对象引用
如果静态方法是同步的,这个同步锁就不是this
了,应该是本类自己,即本类名.class
Lock 接口
同步代码块还有一个问题在于,如果某个线程运行到同步代码块中抛出了异常,那么后面的代码也将不会执行,也就是同步代码块将不再执行
这样引发的一个严重问题就是,锁永远不释放了!
因此JDK1.5后提供了一个Lock接口,给我们带来主动释放的操作
也就是Lock接口可以替换掉synchronized关键字
private int ticket = 10;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(ticket--);
}
lock.unlock();
}
}
同步嵌套和死锁
在java中,同个线程对象的死锁发生是很难的,因为每个线程执行的顺序是一致的,即使在不同的线程对象中,由于要保护的对象数据也不一致,因此也不会用同一个同步锁,这也一定程度上避免线程死锁的问题
但是如果真要恶搞一个死锁,也是可以的:
来看一下这段程序
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
lockA.lock();
System.out.println(Thread.currentThread().getName() + "锁上lockA");
lockB.lock();
System.out.println(Thread.currentThread().getName() + "锁上lockB");
lockB.unlock();
System.out.println(Thread.currentThread().getName() + "释放lockB");
lockA.unlock();
System.out.println(Thread.currentThread().getName() + "释放lockA");
} else {
lockB.lock();
System.out.println(Thread.currentThread().getName() + "锁上lockB");
lockA.lock();
System.out.println(Thread.currentThread().getName() + "锁上lockA");
lockA.unlock();
System.out.println(Thread.currentThread().getName() + "释放lockA");
lockB.unlock();
System.out.println(Thread.currentThread().getName() + "释放lockB");
}
i++;
}
}
通过不断的死循环,增加线程同时进行的概率,并控制一个公共变量,使得不同的线程执行不同的代码,让同一个线程对象也能执行不同顺序的程序
这样虽然每次输出结果都不确定,程序永远不会停止,不是因为死循环,控制台输出也只会死在一个地方不动:
Thread-0锁上lockA
Thread-0锁上lockB
Thread-0释放lockB
Thread-0释放lockA
Thread-1锁上lockA
Thread-0锁上lockB
这就是典型的死锁现象
当然死锁的案例一般不会是这样产生的,能产生的地方,后期会引入一个案例专门讲解
线程通信
之前的线程安全同步问题,虽然能解决多线程对同个资源的互斥操作,但还不能解决多个线程对同一资源的同步操作
比如对同个资源需要两个线程进行不同的操作,而这个操作是有时间同步顺序的,比如线程A进行操作A后线程B才能进行操作B
这样就会可能带来的问题是线程B抢占了线程A的资源进行操作B,而该资源还未进行操作A,显然这样线程就处理不当了!
这也就是经典的生产者和消费者问题的案例
显然,这个问题是同步锁无法解决的
为了解决这个问题,解决手段一般是增加线程通信机制!
线程等待
wait()
这是Object下的方法,每个类都可以直接调用
这是无限等待的机制,只要不被唤醒,就会一直等待下去
线程唤醒
notify()
这是Object下的方法,每个类都可以直接调用
唤醒线程
线程等待和唤醒案例
这个案例完成的是,一个线程负责对一个对象赋值操作,另一个线程对该对象进行取值操作
要求的是这两个线程同步进行,赋值完后取值,取值完后再赋值
大体思路就是设计两个线程,一个输入线程,一个输出线程
输入线程赋值完成后,执行方法wait()永远等待
输出线程输出完成后,注意要先唤醒notify输入线程,自己再wait永远等待
输入线程被唤醒后,即可重新赋值,赋值后注意先唤醒notify输出线程,自己再等待
但是这还是有个问题:
- 如何确保赋值过程中不被输出进程占用来输出
- 如何确保输出线程不先抢占资源来输出(如果这样的话,输出的结果就是未输入的空值了)
对于问题1,解决的方法就是上同步锁,因为问题1本质上就是多线程抢占同一资源的问题
可以看到,即使是资源同步问题,也跟资源互斥问题相关的
而对于问题2,就需要在资源上增加一个信号量的标记状态flag了
这个信号量标志可以自己定义,因此我们定义为
信号量为true说明赋值完成(需要取值)
信号量为false说明取值完成(需要赋值)
因此这么一来,输入输出线程都要根据信号量来决定是否能进行操作,并维护该信号量
对于输入线程,信号量为true,则需要等待,信号量为false,才能进行赋值操作,操作完成后唤醒输出线程,并修改标记
对于输出线程,信号量为false,则需等待,信号量为true,才能进行输出操作,操作完成后唤醒并修改标记
这里需要注意的问题是,怎么准确去唤醒线程,唤醒操作是根据监视器的,因此必须根据锁来唤醒,否则会抛出非法监视器异常
java.lang.IllegalMonitorStateException
根据上述思路
先创建资源类:
public class Resource {
private String name;
private int age;
private boolean signal;
}
在进行输入线程的设计:
@Override
public void run() {
while (true) {
synchronized (r) {
if (r.getSignal()) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
if (i % 2 == 0) {
r.setName("张三");
r.setAge(16);
} else {
r.setName("李四");
r.setAge(20);
}
i++;
r.setSignal(true);
r.notify();
}
}
}
}
输出线程的设计
public void run() {
while (true) {
synchronized (r) {
if (!r.getSignal()) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(r);
r.setSignal(false);
r.notify();
}
}
}
}