一个场景:周末,带着并不存在的女票去看电影,无论是现场买票也好,又或是手机买票也好,上一秒还有位置,迟钝了一下以后,就显示该座位已经无法选中,一不留神就没有座位了,影院的票是一定的,但是究竟是如何做到,多个窗口或者用户同时出票而又不重复的呢? 这就是我们今天所要讲解的多线程问题
需求:我们要实现多线程的程序。
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java是 不能直接调用系统功能 的,所以,我们 没有办法直接实现多线程 程序。
但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
然后提供一些类供我们使用。我们就可以实现多线程程序了。
通过查看API,我们知道了有 2种 方式实现多线程程序。
步骤:
public class MyThread extends Thread{ public MyThread() { } @Override public void run() { for (int i = 0; i < 100; i++){ System.out.println(getName() + ":" + i); } } }
public class MyThreadTest { public static void main(String[] args) { //创建线程对象 MyThread my = new MyThread(); //启动线程,run()相当于普通方法的调用,单线程效果 //my.run(); //首先启动了线程,然后再由jvm调用该线程的run()方法,多线程效果 my.start(); //两个线程演示,多线程效果需要创建多个对象而不是一个对象多次调用start()方法 MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); my1.start(); my2.start(); } } //运行结果 Thread-1:0 Thread-1:1 Thread-1:2 Thread-0:0 Thread-1:3 Thread-0:1 Thread-0:2 ...... Thread-0:95 Thread-0:96 Thread-0:97 Thread-0:98 Thread-0:99
步骤:
public class MyRunnable implements Runnable { public MyRunnable() { } @Override public void run() { for (int i = 0; i < 100; i++){ //由于实现接口的方式不能直接使用Thread类的方法了,但是可以间接的使用 System.out.println(Thread.currentThread().getName() + ":" + i); } } }
public class MyRunnableTest { public static void main(String[] args) { //创建MyRunnable类的对象 MyRunnable my = new MyRunnable(); //创建Thread类的对象,并把C步骤的对象作为构造参数传递 // Thread t1 = new Thread(my); // Thread t2 = new Thread(my); //下面具体讲解如何设置线程对象名称 // t1.setName("User1"); // t1.setName("User2"); Thread t1 = new Thread(my,"User1"); Thread t2 = new Thread(my,"User2"); t1.start() t2.start(); } }
可以避免由于Java单继承带来的局限性
适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想
比如说,某个类已经有父类了,而这个类想实现多线程,但是这个时候它已经不能直接继承Thread类了
(接口可以多实现implements,但是继承extends只能单继承) ,它的父类也不想继承Thread因为不需要实现多线程
//获取线程的名称 public final String getName() //设置线程的名称 public final void setName(String name)
//创建MyRunnable类的对象 MyRunnable my = new MyRunnable(); //创建Thread类的对象,并把C步骤的对象作为构造参数传递 Thread t1 = new Thread(my); Thread t2 = new Thread(my); t1.setName("User1"); t1.setName("User2"); //与上面代码等价 Thread t1 = new Thread(my,"User1"); Thread t2 = new Thread(my,"User2");
//MyThread类中 public MyThread(String name){ super(name);//直接调用父类的就好 } //MyThreadTest类中 MyThread my = new MyThread("admin");
注意:重写run方法内获取线程名称的方式
//Thread getName() //Runnable //由于实现接口的方式不能直接使用Thread类的方法了,但是可以间接的使用 Thread.currentThread().getName()
使用实现Runnable接口方法的时候注意:main方法所在的测试类并不继承Thread类,因此并不能直接使用getName()方法来获取名称。
//这种情况Thread类提供了一个方法: //public static Thread currentThread(): //返回当前正在执行的线程对象,返回值是Thread,而Thread恰巧可以调用getName()方法 System.out.println(Thread.currentThread().getName());
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
//演示如何设置和获取线程优先级 //返回线程对象的优先级 public final int getPriority() //更改线程的优先级 public final void setPriority(int newPriority)
线程默认优先级是5。
线程优先级的范围是:1-10。
线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
在后面的案例中会用到一些,这些控制功能不是很难,可以自行测试。
//线程休眠 public static void sleep(long millis) //线程加入(等待该线程终止,主线程结束后,其余线程开始抢占资源) public final void join() //线程礼让(暂停当前正在执行的线程对象,并且执行其他线程让多个线程的执行更加和谐,但是不能保证一人一次) public static void yield() //后台线程(某线程结束后,其他线程也结束) public final void setDaemon(boolean on) //(过时了但还可以用) public final void stop() //中断线程 public void interrupt()
新建—— 创建线程对象
就绪—— 线程对象已经启动,但是还没有获取到CPU的执行权
运行—— 获取到了CPU的执行权
死亡—— 代码运行完毕,线程消亡
public class SellTickets implements Runnable { private int tickets = 100; @Override public void run() { while (true){ if (tickets > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票"); } } } }
public class SellTicketsTest { public static void main(String[] args) { //创建资源对象 SellTickets st = new SellTickets(); //创建线程对象 Thread t1 = new Thread(st, "窗口1"); Thread t2 = new Thread(st, "窗口2"); Thread t3 = new Thread(st, "窗口3"); //启动线程 t1.start(); t2.start(); t3.start(); } }
在SellTicket类中添加sleep方法,延迟一下线程,拖慢一下执行的速度
通过加入延迟后,就产生了连个问题:
CPU的一次操作必须是原子性(最简单的)的 (在读取tickets--的原来的数值和减1之后的中间挤进了两个线程而出现重复)
随机性和延迟导致的 (三个线程同时挤进一个循环里,tickets--的减法操作有可能在同一个循环中被执行了多次而出现越界的情况,比如说 tickets要大于0却越界到了-1)
也就是说,线程1执行的同时线程2也可能在执行,而不是线程1执行的时候线程2不能执行。
而且这些原因也是以后我们 判断一个程序是否会有线程安全问题的标准
A:是否是多线程环境
B:是否有共享数据
C:是否有多条语句操作共享数据
我们对照起来,我们的程序确实存在上面的问题,因为它满足上面的条件
那我们怎么来解决这个问题呢?
把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行
Java给我们提供了: 同步机制
//同步代码块: synchronized(对象){ 需要同步的代码; }
同步的出现解决了多线程的安全问题
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
任意对象
把同步关键字加在方法上
同步方法的锁对象是谁呢?
this
静态方法的锁对象是谁呢?
类的字节码文件对象。
我们使用 synchronized 改进我们上面的程序,前面线程安全的问题,
public class SellTickets implements Runnable { private int tickets = 100; //创建锁对象 //把这个关键的锁对象定义到run()方法(独立于线程之外),造成同一把锁 private Object obj = new Object(); @Override public void run() { while (true) { synchronized (obj) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票"); } } } } }
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
(可以更清晰的看到在哪里加上了锁,在哪里释放了锁,)
void lock() 加锁 void unlock() 释放锁
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SellTickets2 implements Runnable { private int tickets = 100; private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { try { lock.lock(); ; if (tickets > 0) { try { Thread.sleep(150); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票"); } } finally { lock.unlock(); } } } }
效率低
如果出现了同步嵌套,就容易产生死锁问题
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
我们前面假定的电影院场景,其实还是有一定局限的,我们所假定的票数是一定的,但是实际生活中,往往是一种供需共存的状态,例如去买早点,当消费者买走一些后,而作为生产者的店家就会补充一些商品,为了研究这一种场景,我们所要学习的就是Java的等待唤醒机制
生产者消费者问题 (英语:Producer-consumer problem),也称 有限缓冲问题 (英语:Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
我们用通俗一点的话来解释一下这个问题
解释: 唤醒——让线程池中的线程具备执行资格
Object类提供了三个方法:
//等待 wait() //唤醒单个线程 notify() //唤醒所有线程 notifyAll()
注意:这三个方法都必须在同步代码块中执行 (例如synchronized块),同时在使用时必须标明所属锁,这样才可以得出这些方法操作的到底是哪个锁上的线程
这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。
所以,这些方法必须定义在Object类中。
我们来写一段简单的代码 实现等待唤醒机制
public class Student { String name; int age; boolean flag;// 默认情况是没有数据(false),如果是true,说明有数据 public Student() { } }
public class SetThread implements Runnable { private Student s; private int x = 0; public SetThread(Student s) { this.s = s; } @Override public void run() { while (true){ synchronized (s) { //判断有没有数据 //如果有数据,就wait if (s.flag) { try { s.wait(); //t1等待,释放锁 } catch (InterruptedException e) { e.printStackTrace(); } } //没有数据,就生产数据 if (x % 2 == 0) { s.name = "admin"; s.age = 20; } else { s.name = "User"; s.age = 30; } x++; //现在数据就已经存在了,修改标记 s.flag = true; //唤醒线程 //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。 s.notify(); } } } }
package cn.bwh_05_Notify; public class GetThread implements Runnable { private Student s; public GetThread(Student s) { this.s = s; } @Override public void run() { while (true){ synchronized (s){ //如果没有数据,就等待 if (!s.flag){ try { s.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(s.name + "---" + s.age); //修改标记 s.flag = false; //唤醒线程t1 s.notify(); } } } }
package cn.bwh_05_Notify; public class StudentTest { public static void main(String[] args) { Student s = new Student(); //设置和获取的类 SetThread st = new SetThread(s); GetThread gt = new GetThread(s); //线程类 Thread t1 = new Thread(st); Thread t2 = new Thread(gt); //启动线程 t1.start(); t2.start(); } } //运行结果依次交替出现
public class Student { private String name; private int age; private boolean flag; public synchronized void set(String name, int age) { if (this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.name = name; this.age = age; this.flag = true; this.notify(); } public synchronized void get() { if (!this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(this.name + "---" + this.age); this.flag = false; this.notify(); } }
public class SetThread implements Runnable { private Student s; private int x = 0; public SetThread(Student s) { this.s = s; } @Override public void run() { while (true) { if (x % 2 == 0) { s.set("admin", 20); } else { s.set("User", 30); } x++; } } }
public class GetThread implements Runnable{ private Student s; public GetThread(Student s) { this.s = s; } @Override public void run() { while (true){ s.get(); } } }
public class StudentTest { public static void main(String[] args) { Student s = new Student(); //设置和获取的类 SetThread st = new SetThread(s); GetThread gt = new GetThread(s); Thread t1 = new Thread(st); Thread t2 = new Thread(gt); t1.start(); t2.start(); } }
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法 //创建一个具有缓存功能的线程池 //缓存:百度浏览过的信息再次访问 public static ExecutorService newCachedThreadPool() //创建一个可重用的,具有固定线程数的线程池 public static ExecutorService newFixedThreadPool(intnThreads) //创建一个只有单线程的线程池,相当于上个方法的参数是1 public static ExecutorService newSingleThreadExecutor() 这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法 Future<?> submit(Runnable task) <T> Future<T> submit(Callable<T> task)
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExecutorDemo { public static void main(String[] args) { //创建一个线程池对象,控制要创建几个线程对象 ExecutorService pool = Executors.newFixedThreadPool(2); //可以执行Runnalble对象或者Callable对象代表的线程 pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); //结束线程池 pool.shutdown(); } }
匿名内部类的格式:
new 类名或者接口名( ) { 重写方法; };
本质:是该类或者接口的子类对象
public class ThreadDemo { public static void main(String[] args) { new Thread() { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + i); } } }.start(); } }
public class RunnableDemo { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + i); } } }).start(); } }
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现定义调度的功能
·public Timer() public void schedule(TimerTask task, long delay) public void schedule(TimerTask task,long delay,long period)
abstract void run() public boolean cancel()
Quartz是一个完全由java编写的开源调度框架
如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家 !^_^
如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)
一个坚持推送原创Java技术的公众号:理想二旬不止