多线程
1、线程相关概念
(1)程序(program)
程序视为了完成特定的任务,用某种语言编写的一组指令的集合。也就是我们平时日常生活中写的代码。
(2)进程
进程: 是具有一定独立功能的程序关于一个数据集合的一次运行活动。是资源的分配单位。是程序的一次执行过程,或者是正在运行的一个程序有它自身的产生、存在和消亡的过程。
(3)线程
线程: 又叫轻进程,是进程内的一个相对独立的执行流。是cpu的调度单位。是进程的一个实体,一个进程可以拥有多个线程。
(4) 单线程
同一个时刻,只允许执行一个线程。
(5)多线程
同一个时刻,可以执行多个线程。
(6)并发
同一个时刻,多个任务交替进行。貌似“同时”,实则交替。单核CPU实现多任务就是并发。
(7)并行
同一个时刻,多个任务同时进行。多核CPU就可以实现并行。
2、线程基本使用
线程的使用方式:
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
(1)继承Thread类
示例1:(使用继承Thread实现)
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
//创建cat对象,当做线程
Cat cat = new Cat();
// cat.run(); //这里只是一个普通的run方法,没有真正启动一个线程,把run方法执行完毕之后,才会继续执行
cat.start(); //启动线程,线程自动调用run方法
for (int i = 0; i < 80; i++) {
Thread.sleep(1000);
System.out.println("主线程="+ Thread.currentThread().getName());
}
}
}
//1.当一个类继承了Thread类,该类就是可以当做线程使用
//2、重写run方法,协商自己的业务代码
//3、run Thread 类实现了Runnable接口的run方法
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
*/
class Cat extends Thread {
@Override
public void run() { //重写run,
//每隔一秒,输出“喵喵”
//ctrl + alt + t,快捷键选择功能try,catch
int times = 1;
while (times < 80) {
try {
System.out.println("喵喵~~~~~~~"+times++ + "线程名为:"+ Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
源码分析:
/*底层源码分析
(1)
public synchronized void start() { //调用start0()方法
start0();
}
(2)
//start0()是本地方法,是JVM进行调用的,底层是c/c++实现
//真正实现多线程的效果,是start0(),而不是run
private native void start0();
*/
start()方法调用了start0()方法后,该线程并不一定会马上执行,只是将线程变成了可运行状态。具体什么时候执行取决于CPU,由CPU来统一调度。具体看如下图:
(2)实现Runnable接口(静态代理模式)
- java是单继承的,在某些情况下,一个类以及继承了某个父类,这时候用继承Thread就不太可能了。
- java设计者就提供了另外一个方式创建线程,通过实现Runnable来创建。
举例:
public class Thread02 {
public static void main(String[] args) throws InterruptedException {
//使用runnable实现线程
Tiger tiger = new Tiger();
Thread thread = new Thread(tiger); //代理类thread替代tiger实现了run方法
thread.start(); //代理模式实现线程。
}
}
class animal{}
//单继承,无法继续继承实现Thread,这时候就可以使用代理模式
class Tiger extends animal implements Runnable{
@Override
public void run() {
System.out.println("重写run方法,使用runnable实现线程");
}
}
(3)静态代理模式
使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。弥补java单继承无法实现其他类的缺点。
静态代理实现的步骤:
- 定义一个接口以及实现类
- 创建一个代理类同样实现这个接口
- 将目标实现类注入代理类,然后再代理类的对应方案调用目标类的对应方法。通过代理类屏蔽对目标对象的访问,可以在目标方法中执行一些自己所需要的事情。
举例:
1、定义一个接口类
interface SmsService {
void send(String message);
}
2、创建一个代理类,实现这个接口
class Proxy implements SmsService {
SmsService smsService = null;
public Proxy(SmsService smsService) {
this.smsService = smsService;
}
@Override
public void send(String message) {
System.out.println("代理类实现了send方法");
smsService.send(message);
System.out.println("代理类结束了send方法");
}
}
3、目标类实现接口类
class FinalImpl implements SmsService{
@Override
public void send(String message) {
System.out.println("代理类测试"+message);
}
}
4、实际使用
public class ProxyThread {
public static void main(String[] args) {
Proxy proxy = new Proxy(new FinalImpl());
proxy.send("proxy test()");
}
}
运行上述代码之后,控制台打印出:
代理类实现了send方法
代理类测试proxy test()
代理类结束了send方法
(4)继承 thread 和 实现 runnable 的区别
- 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程 本质没有区别。Thread类本身就实现了Runnable接口
- 实现Runnable接口更适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable
举例:
public static void main(String[] args) {
//两个线程共享一个T1资源
Thread thread1 = new Thread(new T1());
Thread thread2 = new Thread(new T1());
thread1.start();
thread2.start();
}
3、线程终止
- 当线程完成任务后,会自动退出
- 使用变量来控制run方法退出,停止线程,就是通知方式
通知方式结束线程举例:
public class ThreadExit {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
Thread.sleep(10000); //主线程休息一下,再让子线程结束
t.setLoop(false); //终止线程
}
}
class T extends Thread{
private int count = 1;
private boolean loop = true;
//主线程调用set方法就可以终止线程
public void setLoop(boolean loop) {
this.loop = loop;
}
@Override
public void run() {
while(loop){
//休眠一下
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数数:"+count++);
}
}
}
4、线程常用方法
(1)常用方法
setName/getName,设置线程名称/得到线程名称
start 执行线程,java虚拟机调用start0方法启动线程
run 调用线程的run方法
setPriority/getPriority 更改线程优先级/获取线程优先级
sleep 使线程休眠
interrupt 中断线程,比如中断线程的休眠
yield 让出cpu,让其他线程执行,但礼让不一定成功,让出的时间也不确定
join 线程插队,插队的线程插入成功,先执行插队的线程所有的任务
(2)常用方法注意事项
- start底层会创建新的线程,调用run,run就是一个简单的方法调用,不会启动新的线程
- 线程总共有三个优先级(MIN:1,NORM:5,MAX:10)
- interrupt,中断线程,但是没有真正的结束线程。所以一般用于中断正在休眠的线程
- sleep是线程的静态方法,使当前进程休眠
举例:
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
T2 t2 = new T2();
t2.start();
for (int i = 0; i < 20; i++) {
System.out.println("hi"+i);
if(i == 4){
// t2.join(); //先让t2执行
t2.yield(); //尝试先让t2先跑,不一定成功
}
Thread.sleep(100);
}
}
}
class T2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("hello"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(3)用户线程和守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束(平时常用的线程)
- 守护线程:一般是为工作线程服务,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
举例:
public class MyDaemonThread {
public static void main(String[] args) throws InterruptedException {
DaemonThread daemonThread = new DaemonThread();
daemonThread.setDaemon(true); //设置为守护线程,当除了这个线程之外的所有线程结束后,自动结束这个线程
daemonThread.start();
for (int i = 0; i < 10; i++) {
Thread.sleep(500);
System.out.println("主线程运行!");
}
}
}
class DaemonThread extends Thread{
@Override
public void run() {
while(true){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("守护线程一直在守护着你!");
}
}
}
5、线程的生命周期
线程总共有六种状态,其中RUNNABLE状态可以分为Ready,Running两种状态,也可以称为七种状态。
- 线程状态(
State
枚举值代表线程状态):- 新建状态( NEW): 线程刚创建, 尚未启动。
Thread thread = new Thread()
。 - 可运行状态(RUNNABLE): 线程对象创建后,其他线程(比如 main 线程)调用了该对象的
start
方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。 - 阻塞状态(Blocked): 线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。
sleep
,suspend
,wait
等方法都可以导致线程阻塞 - 等待(WAITING): 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING): 该状态不同于
WAITING
,它可以在指定的时间后自行返回。 - 终止(TERMINATED): 表示该线程已经执行完毕,如果一个线程的
run
方法执行结束或者调用stop
方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start
方法令其进入就绪。
- 新建状态( NEW): 线程刚创建, 尚未启动。
状态图:
6、线程同步
(1)线程同步机制
在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术, 保证数据在任何同一时刻,最多只有一个线程访问,保证数据的完整性。
一个线程对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才可以对该内存地址进行操作。
(2)同步具体方法-Synchronized
1、同步代码块
synchronized(对象){//得到对象的锁,才能操作同步代码
//需要被同步的代码
}
2、synchronized 放在方法声明中,表示整个方法为同步方法
public synchronized void m(String name){
//需要被同步的代码
}
7、互斥锁
(1)基本使用
- Java语言中,引用了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应一个可以称为“互斥锁”的标记,这个标记可以用来保证在任一时刻,只能有一个线程访问该对象
- 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能有一个线程访问
- 同步的局限性:导致程序的执行效率变低
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁为当前类本身
举例:
//使用synchronized实现同步机制
class Sell3 implements Runnable{
private static int ticketSum = 100; //多线程共享
private boolean loop = true;
@Override
public void run() {
while(loop){
sell();
}
}
//public static synchronized void sell4(){} 静态方法的锁是加在SellTicketRunnable.class上的
public static /*synchronized*/ void sell4(){//1、方法中加入synchronized实现同步
synchronized (SellTicketRunnable.class){//2、代码块中加入synchronized实现同步,注意锁加在当前类
System.out.println("sell4");
}
}
Object object = new Object();
//public synchronized void sell() 非静态方法同步方法,这时候锁在this中
public /*synchronized*/ void sell(){//1、方法中加入synchronized实现同步
synchronized (/*this*/ object){//2、代码块中加入synchronized实现同步,可以是this,也可以是其他对象
if(ticketSum <= 0){
loop = false;
return;
}
System.out.println("窗口"+Thread.currentThread().getName()+"售出一张票"+
"剩余票数"+ --ticketSum);
}
}
}
(2)注意事项
- 同步方法如果没有使用static修饰,默认锁对象为this
- 如果方法使用static修饰,默认锁对象:当前类.class
- 实现锁的步骤:
- 先分析需要上锁的代码
- 选择同步代码块或者方法,优先选择同步代码块
- 多个线程的锁对象为同一个
8、线程的死锁
(1)基本介绍
多个线程都占用了对方资源,不肯先让,导致死锁,在编程上尽量避免死锁。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件;
- 持有并等待条件;
- 不可剥夺条件;
- 循环等待条件。
9、释放锁
以下操作是释放锁:
- 当线程的同步方法,同步代码块执行结束
- 当前线程在同步代码块,同步方法中遇到break、return
- 当前线程在同步代码块,同步方法中出现了未处理的Error或者Exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
以下操作不会释放锁:
- 当前线程在同步代码块,同步方法中调用Thread.sleep(),Thread.yield()方法暂停当前线程的执行,不会释放锁
- 线程执行同步代码块中,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。尽量不用suspend()和resume()来控制了,已经不推荐使用