程序不停地在屏幕上输出一句问候的语句(比如“你好”),同时,当我通过键盘输入固定输入的时候,程序停止向屏幕输出问候的语句(比如说输入gun)。
用我之前所学习的技术似乎无法做到,这时候就需要了解多线程相关知识。
如何理解线程?
从代码的执行路径的角度来理解线程。
首先,Java程序中所有的代码都运行在一条执行路径当中,而一条执行路径其实就是一个线程。换言之,我们所有的代码都是运行在某个线程当中的。
那么什么是进程呢?一个进程就代表一个运行中的程序(这并非是进程的概念,只是形象化的理解)
前提:计算机中,cpu就是专门用来做运算,一切运算都是由cpu,而且cpu是计算机当中最宝贵的资源,事实上我们是把cpu的用来计算的时间当作是一种资源。
只有一个cpu,单核情况下。
单道批处理:
单道:在整个操作系统当中,同一时间内内存中只有一个程序运行,程序的运行只能是上一个程序运行完才开始运行下一个程序。
批处理:程序运行过程中,不会有任何响应,一次执行完毕。
单道批处理操作系统,它并不能很好的利用cpu的计算时间。
假设,在单道批处理系统中运行了一个程序,在他的程序中需要执行IO(例如和打印机传输数据),在IO的数据传输过程中,有很大一部分时间是不会用到cpu的计算功能的,此时cpu闲置。
多道批处理操作系统
多道:在操作系统中,内存中同时可以有多个应用程序在运行,这样一来一旦某个程序不需要使用cpu的计算功能,操作系统就会把cpu的计算时间分配给内存中其他的应用程序在运算,这样一来大大提高了cpu的利用效率。
应用程序的执行:在单核情况下,一个cpu在同一时间点只能为一个应用程序服务。当程序运行,占用cpu执行时间,这个应用才算真正的执行。在多道批处理系统中,多个应用程序交替执行,看起来好像在“同时”运行。
核心原因:进程的交替执行,交替过程是需要付出额外代价的——进程的上下文切换。
上下文切换
程序的运行数据在运行时要放到cpu的寄存器里,供cpu的运算器来运算,对于单进程来说,多个程序交替运行的过程中,为了保证程序之间的运行数据不相互干扰。
- 在程序切走的时候,该进程失去了cpu的使用权,在被其他进程占用cpu之前,要利用cpu功能,将当前这一刻计算机等相关状态保存起来。
- 当该进程重新获取cpu执行权,开始执行之前利用cpu的功能以及刚刚保存起来的上下文,把计算机恢复到上次切走时的程序执行状态。
上下文切换是一个比较耗时的工作,而且需要使用到cpu(cpu虽然不闲了,但是被加大了无关于程序本身运行的工作量,这就是额外的代价)。
现代操作系统
引入了另外一个东西:线程。
线程,又被称为轻量级进程,一个进程可以有多个线程。同一个进程的多个线程中,线程的上下文切换的时候付出的额外代价小得多——进一步提高cpu的利用率。
以线程为单位占用cpu,同一个进程的多个线程,共享一个进程的数据。
通常生活中所说的同时,指的是并行。
结论:java语言当中利用多线程,实现的“并发”concurrent的“同时”效果。
1.Java命令运行一个Java程序的过程?
java 主类类名(命令行打入Java 类名,运行这个java程序)
2.JVM是单线程还是多线程?
jvm是多线程的,其实至少还应该有另外一个垃圾回收器线程在执行。
例如:当堆上的某个对象没有引用变量指向它的时候,该对象就变成垃圾,被同时运行的垃圾回收器线程回收,保障堆上的内存不会被用完。
一个Thread类(Thread子类)对象代表一个线程。
为什么我们重写Thread类中的run方法?
只有Thread run()方法中的代码,才会执行在子线程中。为了保证子线程中运行的是我们想要在子线程中运行的代码,必须重写run()方法。
但是,如果想要让代码,在子线程中运行,并非一定,代码要写在run方法方法体中。
对于定义在该Thread子类中,其他方法方法体中的代码,也可以运行在子线程中。换句话说,一个方法,被哪个线程中的代码调用,被调用的方法,就运行在调用它的线程中。
启动线程,必须使用start()方法来启动,这样才能使得Thread中的run方法运行在子线程中。
如果通过调用run方法,来执行Thread的run方法代码,这仅仅只是普通的方法调用。
同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次。如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象。
线程调度是指系统为线程分配处理器使用权的过程。
假设在单CPU的情况下线程的两种主要调度模型:
协同式线程调度(Cooperative Thread-Scheduling)
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制。当他把自己的工作完成之后,会主动通知系统切换到另外一个线程上去。
好处:实现简单。
缺点:线程执行时间不可控,不稳定
抢占式调度(Preemptive Thread-Scheduling)
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。
好处:现成的执行时间是可控的,根据线程的优先级来控制线程的切换以及执行时间。线程的优先级是动态可变的(随等待时常增长而增加,随执行时长而降低)。
抢占式调度。
Java中的优先级之所以会不太可靠:
public final String getName()
public final void setName(String name)
public static Thread currentThread()//返回当前正在执行的线程对象的引用。该方法在哪个线程中被调用,那个线程就是该方法的当前线程。
思考,如何获取main线程的名称呢?
只要能拿到main方法所在线程的线程对象即可。在主方法中调用用currentThread()方法。
注意事项:
多线程的优先级的取值范围1 <= priority <=10
线程的默认优先级为5
然而给线程对象设置优先级并没有什么用。
事实上我们在Java语言中设置的线程优先级,他仅仅只能被看成是一种“建议”(给操作系统的建议)。实际上操作系统本身有它自己的一套优先级(静态优先级+动态优先级)。
java官方:线程优先级并非完全没有用,我们Thread的优先级它具有统计意义,总的来说高优先级的线程占用cpu的执行时间多一点,低优先级的线程占用cpu的时间少一点。
结论:千万不要试图通过设置线程的优先级控制线程的执行的先后顺序
多线程带来了并发,但是同时多线程也给我们的程序带来了不确定性。
public static native void sleep (long millis)//在指定的毫秒数内让正在执行的线程休眠(暂时暂停)。指定的休眠时间以毫秒为单位。
被native修饰的方法称之为本地方法,本地方法都不是由java语言实现的。
public final void join()//等待该线程终止。
线程对象.join(),”该线程“:在哪个线程对象上调用join方法,该线程指的就是哪个线程对象所表示的线程。
谁等待?当前线程等待,join线程在哪个线程中被调用,哪个线程就等待。
等待谁?该线程,在哪个线程对象上调用join方法,等待的就是哪个对象的线程。
主线程等joinThread线程执行完毕再继续执行。
public static void yield()//(礼让方法)
暂停当前正在执行的线程对象(自己放弃cpu的使用权,yield方法可以实现)
执行其他线程。(yield方法不一定实现这个功能)
线程放弃cpu的使用权之后就会进入等待状态,此时cpu会在众多等待线程中再选择一个,不排除选到刚才自己放弃使用权的线程。
不确定性导致实际开发中也不会使用yield方法来进行线程调度。(Demo中可能会看到。)
public final void setDaemon(boolean on)//将该线程标记为守护线程(参数为true时)或用户线程(参数为false时)(我们创建出来的普通线程)。
运行特征:
当正在运行的线程都是守护线程,jvm直接退出(结束执行)。
使用场景:垃圾回收器实际上就运行在守护线程中。
该方法必须在线程启动前调用。
public void interrupt()//中断线程(打断线程的阻塞状态)
如果该线程在调用wait方法或join方法或sleep方法过程中受阻,线程将收到一个InterruptedException。
interrupt方法可以打断线程的休眠或者等待,跳转到catch分支,这意味着这个子线程中try块中的功能被异常终止了。
实际在应用层很少去使用,但是不是不用(比较底层的代码例如线程池会用到interrupt方法)。
新建:线程处于刚刚创建的状态。start方法。
就绪:有执行资格,等待cpu调度获得执行权。万事俱备只欠cpu,除了cpu资源,之外的程序执行条件都已经满足了。
运行:取得执行权,正在cpu上执行。优先级降低到某种程度后,操作系统会剥夺线程的cpu资源,调度其他优先级较高的就绪态线程,此时若上一个线程还没运行完,他又会回到就绪态。
阻塞:无执行资格,无执行权。处于执行态的线程如果调用了sleep(),join(),或者执行IO,就会处于等在状态,此时不该让他继续占用cpu,处于阻塞状态。
除了缺少cpu资源之外,还需要满足其他未满足的线程执行条件。当执行条件重新满足之后,就会再次转回就绪态,等待下一次被调度。
定义实现Runnable接口的子类。
创建该子类对象
在创建Thread对象的时候,将创建好的Runnable子类对象作为初始化参数,传递给Thread对象。
Thread并不是一个抽象类,我们是可以直接new Thread类的对象的。
启动Thread对象(启动线程),Thread.start();
注意事项:
- 我们的Runnable接口子类的run方法中的代码,会运行在子线程当中。
- 所以在线程的第二种实现方式当中,我们自己来定义子类,实现Runnable接口的run方法,将要在子线程中执行的代码在run方法中。
- 但是runnable子类对象并不代表一个线程,他只代表线程中执行的任务。
逻辑上说,第二种实现方法逻辑更加清晰。
Runnable接口子类对象的run方法运行在Thread接口中的原因图解。
多线程仿真如下场景:
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。
分析:
//Ver1.0,实现方式一
class SalesWindow extends Thread {
int tickets = 100;
@Override
//售票
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
System.out.println(getName()+"售出了第"+(this.tickets--)+"张票");
}
}
}
三个窗口各买各的,每个窗口卖一百张,总共卖了三百张,功能实现失败。若要实现数据共享可以在tickets前加static修饰,使所有对象共享它。
//Ver2.0,实现方式二
class SalesTask implements Runnable {
int tickets = 100;
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
三个线程都使用一个功能,实现了数据共享。
在不考虑其他因素的前提下,通过多线程程序模拟上述的售场景,基本没啥问题。
但我们的程序其实还可以做的更真一些,因为实际情况下,售票数据通过网络传输,总是存在一些延迟的情况。
所以在真正售出一张票后,需要一段时间,才可以真正去修改剩余票数。
因此,我们可以在程序中,增加对于售票延迟的模拟,即每次卖票延迟100ms,后再去修改剩余票数
class SalesTaskVer2 implements Runnable {
private int tickets = 100;
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
当增加售票延迟之久,出现了一些明显的错误。
其实不管是超卖还是多卖问题,都属于典型的多线程的数据安全问题。
在多线程运行环境下,在多个线程访问线程之间共享的数据时,访问到了错误的共享数据的值。不管是超卖还是多卖问题,都属于典型的多线程的数据安全问题。
多线程运行环境。
需求决定,无法打破
数据共享。
需求决定,无法打破
共享数据的非原子操作。
原子操作:就是一组不可分割的操作。这组操作要么一次全部完成,要么一步都不做。当某个线程在进行原子操作的过程中产生了线程的切换,其他线程访问了这个时刻的中间状态,这可能会导致多线程共享数据错乱。
可以打破
解决多线程数据安全问题一一>如何将多线程中对共享变量一组的操作变成原子操作。
打破以上三个产生原因之一即可。
两种思路构造原子操作(一个线程对共享数据的访问一次完成)
synchronized(锁对象){
//一组要作为原子操作的代码(需要访问共享数据)
}
synchronized代码块中的锁对象可以是java语言中的任意一个对象(仅限于synchronized代码块语句中)。
我们的代码都是在某一条执行路径(某一个线程)中运行,当某个线程执行到synchronized代码块时,会尝试在当前线程中对锁对象加锁。
加锁线程何时释放锁?
当枷锁线程执行完了同步代码块中的代码(对共享变量的一组操作),在推出同步代码块之前jvm会自动清理锁对象的标志位,将锁对象变成未上锁状态(释放锁)。
class SalesTaskVer3 implements Runnable {
private int tickets = 100;
Object lockObj = new Object();//锁对象
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockObj) {
//double check
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
}
}
千万要注意:
- 虽然synchronized代码块中的锁对象可以是java语言中的任意对象。
- 但是在多线程运行环境下,想要让访问同一个共享变量的多个synchronized代码块中的代码是原子操作,那么对同一个共享变量的访问必须使用同一个锁对象!
解决多线程数据安全问题是通过加锁,构造一个线程对共享变量的原子操作。但其实加锁其实就是在完成线程同步。
所以,最终,其实我们是用线程同步来解决线程数据安全问题的。
线程同步优缺点:
整个方法就是一个同步代码块,其效果等价于同步代码块。同步方法的锁对象是this.当前对象(锁对象是隐式给出的)
class SalesTasknew implements Runnable {
private int tickets = 100;
Object lockObj = new Object();
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.salesingLock();
}
}
private synchronized void salesingLock() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
}
}
静态方法也可以是同步方法。静态方法通过类名.静态方法()调用。静态方法依赖于类而存在,而jvm中一个类对应一个class对象,所以一个静态方法锁对象就是表示静态方法所属类的Class对象(隐式给出的)。
synchronized关键字虽然可以理解加锁,解锁的原理,但是却看不到具体的过程。实现同步代码块,除了使用synchronized之外,其实JDK1.5之后,提供了另外的方式Lock锁机制。
利用线程同步,解决多线程的数据安全问题,有两种方式:
synchronized锁对象只提供了用来模拟锁状态的标志位(加锁和释放锁),但是加锁和释放锁都是由jvm隐式完成的,和锁对象本身无关,所以synchronized锁对象不是一把完整的锁。
一个Lock对象,就代表一把锁,而且还是一把完整的锁,Lock对象,它如果要实现加锁和释放锁,不需要synchronized关键字配合,它自己就可以完成。
Lock:loch()加锁;unlock释放锁。
两种锁对象,实现方式完全不同
class SalesTasknew implements Runnable {
private int tickets = 100;
//常用子类:ReentrantLock
Lock lock = new ReentrantLock();
@Override
public void run() {
//当票没卖完,就一直卖票
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//this.salesingLock();
lock.lock();
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
}
lock.unlock();
}
}
}
联系:都可以实现线程同步
synchronized(锁对象){需要同步的代码}
lock.lock()
需要同步的代码
lock.unlock()
推荐使用synchronized+java对象完成线程同步。
Lock l=......;
l.lock();
try{
//
}finally{
//不管正常还是异常都可以释放这把锁
l.unlock();
}
同步另一个弊端:如果出现了嵌套锁,可能产生死锁。
某个线程要同时处有两把锁lockA和lockB,换个说法,这个线程在成功持有lockA锁的情况下,再持有lockB锁。
synchronized(lockA){
//当某线程的代码执行到这里
synchronized(lockA){
//执行到这里,在成功持有lockA锁的情况下,再持有lockB锁,此时当前线程就同时持有两把锁
}
}
public class Exnew02 {
public final static Object lockA = new Object();
public final static Object lockB = new Object();
}
class ABThread extends Thread {//线程一
@Override
public void run() {
synchronized (Exnew02.lockA) {
//访问共享变量,计算,得到一些中间结果
System.out.println("ABThread,A锁");//输出了
synchronized (Exnew02.lockB) {
//访问打印机,打印中间结果
System.out.println("ABThread,B锁");
}
}
}
}
class BAThread extends Thread {//线程二
@Override
public void run() {
//尝试访问打印机
synchronized (Exnew02.lockB) {
//获得打印机访问权
System.out.println("BAThread,B锁");//输出了
synchronized (Exnew02.lockA) {
//再获取共享变量访问权,进行计算
System.out.println("BAThread,A锁");
}
}
}
}
死锁是指两个以上的线程在执行过程中,因为争夺资源而产生的一种相互等待的现象。
解决方式:
调整线程获取多把锁的顺序,将多个线程中获取锁的顺序换成一样的。
要么一个线程同时持有所需要的多把锁,要么一把锁都不加。实质就是把加多把锁作为一个原子操作。
再定义一把新锁,利用这把锁(synchronized+该对象),实现将加多把锁的操作变成一个原子操作。
多个生产者和多个消费者各自都是以异步的方式运行
但是在某些情况下,生产者和消费者之间必须保持协作:
- 当缓冲区空的时候,不允许消费者到缓冲区中取数据。
- 当缓冲区满的时候,不允许生产者向缓冲区中放入数据。
- 同时缓冲区中的一个单元,只能放入一个产品。
- 同时还要注意,因为生产者和消费者都是异步的,但是它们都共享缓冲区!!
Java中主要通过Object中的方法来实现:
public final void wait()//阻止自己,在其他线程调用此对象的notify方法或notifyAll方法前导致当前线程等待。
wait方法的阻塞条件:
在当前线程中,如果在对象上调用wait的方法,就会导致当前线程阻塞。
wait方法的的唤醒条件:
首先有一种说法,当某线程因为在某对象上调用了wait方法,处于阻塞状态,我们就说该线程在该对象上阻塞。
如果要唤醒在某对象上阻塞的线程,就必须在其他线程中,在同一(线程阻塞的)对象上调用notify或notifyAll方法。
wait方法的使用条件:
当前线程必须拥有此对象监视器。当前线程持有该锁对象(把该对象当成锁对象,并让当前线程加锁)。
wait方法执行特征(锁对象的角度):
该线程释放(release)对此监视器的所有权并等待。释放锁对象的持有。
理解:wait方法只能被一个线程的锁对象调用。当调用wait方法后,当前线程先释放锁,进入阻塞状态等待下次被调用。当有其他线程中的notify或者notifyAll被调用,通知这个被阻塞的线程,这个线程就会进入就绪态等待cpu的调度,被调度后先再次加锁,然后接着上次执行被切走的地方继续执行。
notify()//通知别人
唤醒在此对象监视器(锁)上等待的线程;如果有多个线程都在此对象上等待,则选择一个线程,选择是随机的并在对实现做出决定时发生。
notifyAll()//通知别人
唤醒在此对象监视器上阻塞的所有线程。
/第二版蒸笼,智能蒸笼,完成功能
//线程同步:利用同步方法,都在同一个container上调用同步方法
//线程通信(协作)wait,notifyAll
class Container {
private Food food;
//消费者用
public synchronized void eatFood() {
if (food == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "吃了" + food);
food = null;
this.notifyAll();
}
}
//生产者用
public synchronized void setFood(Food newfood) {
if (food == null) {
//newfood是PDTask类里的新做的食物,把它放到蒸笼里
food = newfood;
System.out.println(Thread.currentThread().getName() + "做了" + food);
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Food {
private double price;
private String name;
public Food(double price, String name) {
this.price = price;
this.name = name;
}
@Override
public String toString() {
return "Food{" +
"price=" + price +
", name='" + name + '\'' +
'}';
}
}
class ConsumerTask implements Runnable {
private Container container;
public ConsumerTask(Container container) {
this.container = container;
}
@Override
public void run() {
while (true) {
container.eatFood();
}
}
}
class PDTask implements Runnable {
private Container container;
public PDTask(Container container) {
this.container = container;
}
private Food[] foodMenue = {new Food(30, "蟹黄包"), new Food(3, "豆沙包"),
new Food(8, "小笼包"), new Food(6, "牛肉包")};
@Override
public void run() {
while (true) {
container.setFood(foodMenue[new Random().nextInt(foodMenue.length)]);
}
}
}
当有多个生产者消费者的时候,一定要用notifyAll通知别人。
面试题
Thread.sleep() VS Object.wait()
概念
线程池就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了侵犯创建线程对象的操作,无需反复创建线程而消耗过多的资源。
线程池容器——集合:ArraryList,HashSet,LinkedList(推荐),HashMap
底层原理
JDK1.5之后,JDK中内置了线程池可以直接使用。
线程池优点:
线程池:JDK1.5之后提供的。
java.util.concurrent.Executors:线程池的工厂类,用来生成线程池。
Executors类中的静态方法
static ExercutorService newFixedThreadPool(int nThreads)//创建一个可以复用固定数量线程的线程池。
ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务。
submit(Runnable task)提交一个Runnable任务用于执行。
关闭和销毁线程池的方法:void shutdown()
线程池的使用步骤: