多线程基础
目录1.相关概念(程序、进程、线程)
程序 = 数据结构 + 算法;
进程 = 程序的一次执行。
线程是进程中实际运作单位,是操作系统能够进行运算调度的最小单位,例如main()。
并发多个线程交替执行,是一种貌似“同时执行”的错觉,在单CPU时,多任务就是并发执行。
并行多个线程同时执行。
2.Java线程创建
2.1 继承Thread类
重写run()方法,run()方法内部为线程要执行的程序。
使用继承了Thread的类实例化来调用start()方法启动线程。
不调用run()方法来启动线程的原因是,run()方法为该类中的一个普通方法,当程序运行至调用run()方法的语句时,会执行完run()方法再继续往下执行。
调用start()方法后,线程不一定会立马执行,而是将线程转换为就绪状态,等待cpu调度执行。
2.2 实现Runnable接口
由于Java单继承的限制,若某个类已经继承了另一个类,但这个类仍然要变成线程类,就需要实现Runnable接口。
与继承Thread类不同的是,实现Runnable接口的类在启动线程时,无法通过对象调用start()方法。
在这里用了一种设计模式(静态代理)的方法来启动线程。
//这里pig为Runnable接口的一个实现类
Runnable pig = new Pig();
Thread thread = new Thread(pig);
thread.start();
2.3 继承Thread类和实现Runnable接口的区别
实现Runnable接口适用于多个线程共享资源(e.g 变量)的情况,且避免了Java单继承的限制。
若是继承Thread类的线程类需要多个线程访问同个变量则需要将变量申明为静态变量
由于实现Runnable接口启动线程时需要通过静态代理的方法,此时我们可以使用多个Thread对象来启动同一个Runnable实现类的对象。
//这里pig为Runnable接口的一个实现类
Runnable pig = new Pig();
Thread thread1 = new Thread(pig);
Thread thread1 = new Thread(pig);
thread1.start(); // 线程1访问实例pig的资源
thread2.start();// 线程2同样访问实例pig的资源
3.线程终止
3.1 正常终止
在线程正常运行完它所要执行的程序时它会自然终止。例如main方法中所有语句运行完后会自然停止程序,或新建线程的run方法中程序运行完后同样会自然终止。只有当一个进程的所有线程都运行完后,进程才会结束。
3.2 通知方式
在某些线程的程序中,我们可能需要它一直执行到某个条件达成时再终止。当其达成我们想要的条件时需要通过通知方式来结束我们想要结束的线程。举个例子
/**
* 通知方式结束线程
*/
public class threadTest05 {
public static void main(String[] args) throws InterruptedException {
Th3 t = new Th3();
Thread thread = new Thread(t);
thread.start();
Thread.sleep(10000);//主线程睡眠10秒,当满足 睡眠等于10秒 的条件后通知线程thread终止
t.setFlag(false);//通过修改中间属性 通知线程thread终止
}
}
class Th3 implements Runnable{
//用于通知线程结束的属性
private boolean flag = true;
int i = 0;
//线程每1秒运行一次
@Override
public void run() {
while(flag){
System.out.println("Hi!No."+ (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
4.线程常用方法
1.setName //设置线程名称
2.getName
3.start //启动线程
4.run//调用线程对象run方法
5.setPriority//更改线程优先级
6.getPriority//获取优先级
7.sleep //使正执行的线程休眠指定毫秒数
8.interrupt // 中断线程
9.yield // 线程礼让。让其他线程先执行,但礼让时间不确定,不一定礼让成功
10.join // 线程插队。插队的线程一旦插队成功,则先执行完插入的线程的所有程序
要插队的线程要处于就绪状态。
5.用户线程与守护线程
5.1 概念
用户线程也叫工作线程,当线程任务执行完或通知方式结束。
守护线程一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束。常见的如垃圾回收机制。
5.2 区别
用户线程的结束需要主动结束或者执行完程序,与其他的线程死或运行无关。
5.3 实现守护线程
调用setDaemon(boolean b)方法来使线程成为守护线程。
//myDaemonThread为一个线程类
//将线程启动前要调用setDaemon方法使线程成为守护线程
myDeamonThread md = new myDeamonThread();
Thread thread = new Thread(md);
thread.setDaemon(true);
thread.start();
6.线程状态
7.线程同步(关键字Synchronized)
7.1 线程同步概念
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
7.2 实现同步
7.3.1 同步方法 | 添加关键字Synchronized
public Synchronized void m(){
//代码块
}
创建一个线程类 sellWindows用于售卖100张门票(ticket),在主函数中创建两个线程来通过方法sellTicket访问线程类中ticket属性。
public class threadTest06 {
public static void main(String[] args) {
sellWindows one = new sellWindows();
Thread thread1 = new Thread(one);
Thread thread2 = new Thread(one);
thread1.setName("SSS");//设置窗口一名字
thread2.setName("QWE");//设置窗口二名字
thread1.start();
thread2.start();
}
}
class sellWindows implements Runnable{
private int ticket = 100;//票数100
private boolean flag = true;
@Override
public void run() {
while(flag){
sellTicket();
}
}
/*
省略get/set方法
*/
//售票同步方法
public synchronized void sellTicket(){
if(getTicket() == 0) {
System.out.println("售空。");
setFlag(false);
return;
}
setTicket();
System.out.println("窗口"+Thread.currentThread().getName()+"售出一张票,余票"+getTicket());
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果
左图sellTicket方法不是同步的,右图sellTicket方法是同步的,很明显的看到非同步方法对属性进行访问时会出现多个线程同时修改数据的情况。而右图则没有这种情况,每次只有一个线程能够调用同步方法对属性进行修改。
7.3.2 同步代码块|
同步代码块需要给指定对象obj加锁
synchronized (obj){
/*程序*/
}
8.锁
8.1 锁简述
锁:可以简单理解为一种访问权限——排他性访问权。当某个线程拥有锁时,将独占对某实例内存的访问权,其他线程只能等待已进入实例线程执行完毕。
加锁目的:实现多线程同步访问相同内存。例如实例属性。
加锁位置:①非静态同步方法(实例方法)和同步代码块加锁位置为this对象,同步代码块可以为其他对象加锁。当某个对象实例加锁后,多个线程共同访问该对象实例时需要等待获得对象的锁。
②静态同步方法,由于内存与类一齐加载,为类所有,因此静态同步方法的加锁位置为方法所在的类。
8.2 死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
线程T1拥有资源O1,等待资源O2然后释放O1,此时线程T2拥有资源O2,等待资源O1释放O2,此时造成死锁。