学习目标
- 线程概念
- Java实现多线程程序一
- Thread类的方法
- Java实现多线程程序二
- 线程安全问题
- 同步synchronized使用
- 锁对象的选择
- 死锁案例
- 生产者与消费者
- JDK5特性JUC
- 单例模式
- 关键字volatile
- 线程池
- ConcurrentHashMap
1. 线程的基本概念
1.1 进程
任何的软件存储在磁盘中,运行软件的时候,OS(操作系统)使用IO技术,将磁盘中的软件的文件加载到内存,程序才能运行。
进程的概念:应用程序(typerpa、word、IDEA)运行的时候进入到内存,程序在内存中占用的内存空间(进程)。
1.2 线程
线程(Thread):在内存和CPU之间,建立一条连接通路,CPU可以到内存中取出数据进行计算,这个连接的通路,就是线程
一个内存资源:一个独立的进程,进程中可以开启多个线程 (多条通路) 。
并发:同一个时刻多个线程同时操作了同一个数据。
并行:同一个时刻多个线程同时执行不同的程序。
2. Java实现线程程序
今天之前的所有程序都有一个共性:main启动之后,一条线走到底 (单线程)
2.1 java.lang.Thread类
第一种方法:继承的方式实现多线程。
一切都是对象,线程也是对象,Thread类是线程对象的描述类
- 实现线程程序的步骤:
- 定义类继承Thread
- 子类重写方法run
- 创建子类对象
- 调用子类对象的方法start()启动线程
//- 定义类继承Thread //- 子类重写方法run public class SubThread extends Thread { public void run(){ for(int x = 0 ; x < 50 ;x++) System.out.println("run..."+x); } }
public static void main(String[] args) { //创建线程程序 SubThread subThread = new SubThread(); //调用子类对象的方法start()启动线程 //启动线程,JVM调用方法run subThread.start(); for(int x = 0 ; x < 50 ;x++) System.out.println("main..."+x); }
(1)调用子类对象的方法start()启动线程。我们只需要调用start()方法,run()方法不是我们自己调用的。
(2)启动线程,JVM调用方法run。
2.2 线程的内存图
2.3 Thread类的方法
- Thread类的方法 getName()返回线程的名字,返回值是String类型
public class ThreadName extends Thread { public void run (){ System.out.println("线程名字:: "+ super.getName()); } }
public static void main(String[] args) { ThreadName threadName = new ThreadName(); //threadName.setName("旺财"); threadName.start(); ThreadName threadName1 = new ThreadName(); //threadName1.setName("小强"); threadName1.start(); }
- Thread类静态方法 : Thread currentThread()
- 静态调用,作用是返回当前的线程对象
- "当前" , 当今皇上——本地主机
//获取当前线程对象,拿到运行main方法的线程对象 Thread thread = Thread.currentThread(); System.out.println("name::"+thread.getName());
- Thread类的方法 join()
- 解释:执行join()方法的线程,他不结束,其它线程运行不了
public static void main(String[] args) throws InterruptedException { JoinThread t0 = new JoinThread(); JoinThread t1 = new JoinThread(); t0.start(); t0.join(); t1.start(); }
- Thread类的方法 static yield()
- 线程让步,线程把执行权让出
public void run() { for(int x = 0 ; x < 50 ;x++){ Thread.yield(); System.out.println(Thread.currentThread().getName()+"x.."+x); } }
3. Java实现线程程序
3.1 java.lang.Runnable接口
第二种实现多线程的方法:用接口来实现
- 实现线程程序的步骤 :
- 定义类实现接口
- 重写接口的抽象方法run()
- 创建Thread类对象
- Thread类构造方法中,传递Runnable接口的实现类对象
- 调用Thread对象方法start()启动线程
//- 定义类实现接口 // - 重写接口的抽象方法run() public class SubRunnable implements Runnable{ @Override public void run() { for(int x = 0 ; x < 50 ;x++){ System.out.println(Thread.currentThread().getName()+"x.."+x); } } }
public static void main(String[] args) { //创建接口实现类对象 Runnable r = new SubRunnable(); //创建Thread对象,构造方法传递接口实现类 Thread t0 = new Thread(r); t0.start(); for(int x = 0 ; x < 50 ;x++){ System.out.println(Thread.currentThread().getName()+"x.."+x); } }
3.2 实现接口的好处
接口实现好处是设计上的分离效果:线程要执行的任务和线程对象本身是分离的。
继承Thread重写方法run():Thread是线程对象,run()是线程要执行的任务。
实现Runnable接口:方法run在实现类,和线程无关,创建Thread类传递接口的实现类对象,线程的任务和Thread没有联系,,解开耦合性。
4. 线程安全
出现线程安全的问题需要一个前提:多个线程同时操作同一个资源。
线程执行调用方法run,同一个资源是堆内存的。
4.1 售票例子
火车票的票源是固定的,购买渠道在火车站买,n多个窗口。
public class Ticket implements Runnable { //定义票源 private int tickets = 100; @Override public void run() { while (true) { if (tickets > 0) { try { Thread.sleep(10); } catch (Exception e) {} System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张"); tickets--; }else{ break; } } } }
public class ThreadTest { public static void main(String[] args) { //这个属于一个种子,被三个线程共享 Ticket ticket = new Ticket(); //创建3个窗口,3个线程 Thread t0 = new Thread(ticket); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); t0.start(); t1.start(); t2.start(); } }
我们可以发现发生了线程安全问题:多个线程抢同一张票源。
解决线程的安全问题:当一个线程没有完成全部操作的时候,其它线程不能操作。
4.2 同步代码块
同步代码块可以解决线程安全问题:格式 synchronized关键字。
synchronized(任意对象){ //线程操作的共享资源 }
任意对象:在同步中这个对象称为对象锁,简称锁,官方的稳定称为对象监视器。
同步代码块,如何保证线程的安全性。
- 同步代码块的执行原理:关键点就是对象锁
- 线程执行到同步,判断锁是否存在
- 如果锁存在,获取到锁,进入到同步中执行
- 执行完毕,线程出去同步代码块,将锁对象归还
- 线程执行到同步,判断锁所否存在
- 如果锁不存在,线程只能在同步代码块这里等待,锁的到来
使用同步:线程要先判断锁,然后获取锁,出去同步要释放锁,增加了许多步骤,因此线程安全运行速度慢,牺牲性能,不能牺牲数据安全。
4.3 同步方法
当一个方法中,所有代码都是线程操作的共享内容,可以在方法的定义上添加同步的关键字 synchronized ,同步的方法,或者称为同步的函数。
- 同步方法中有对象锁吗?有且是this对象
- 静态同步方法中有对象锁吗?锁对象是本类.class属性。 这个属性表示这个类的class文件的对象。
@Override public void run() { while (true) sale(); } private static synchronized void sale(){ // synchronized (Ticket.class) { if (tickets > 0) { try { Thread.sleep(20);//线程休眠,暂停执行 } catch (Exception ex) { } System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张"); tickets--; } // } }
5. 死锁
死锁程序:多个线程同时争夺同一个锁资源,出现程序的假死现象。
面试点:考察开发人员是否充分理解同步代码的执行原理
同步代码块:线程判断锁,获取锁,释放锁,不出代码,锁不释放
- 死锁代码
/** * 实现死锁程序 */ public class ThreadDeadLock implements Runnable{ private boolean flag ; public ThreadDeadLock(boolean flag){ this.flag = flag; } @Override public void run() { while (true){ //同步代码块的嵌套 if (flag){ //先进入A锁同步 synchronized (LockA.lockA){ System.out.println("线程获取A锁"); //在进入另一个同步B锁 synchronized (LockB.lockB){ System.out.println("线程获取B锁"); } } }else { //先进入B锁同步 synchronized (LockB.lockB){ System.out.println("线程获取B锁"); //再进入另一个同步锁A锁 synchronized (LockA.lockA){ System.out.println("线程获取A锁"); } } } } } }
public class LockA { public static LockA lockA = new LockA(); }
public class LockB { public static LockB lockB = new LockB(); }
public static void main(String[] args) { ThreadDeadLock threadDeadLock = new ThreadDeadLock(true); ThreadDeadLock threadDeadLock2 = new ThreadDeadLock(false); new Thread(threadDeadLock).start(); new Thread(threadDeadLock2).start(); }
6. JDK5新特性Lock锁
JDK5新的特性:java.util.concurrent.locks包。定义了接口Lock。
Lock接口替代了synchronized,可以更加灵活
- Lock接口的方法
- void lock() 获取锁
- void unlock()释放锁
- Lock接口的实现类ReentrantLock
/** * 优化为juc包的接口Lock */ public class Ticket implements Runnable { //定义票源 private int tickets = 100; //获取Lock接口的实现类对象 private Lock lock = new ReentrantLock(); @Override public void run() { while (true) sale(); } private void sale(){ //获取锁 lock.lock(); if (tickets > 0) { try { Thread.sleep(20);//线程休眠,暂停执行 } catch (Exception ex) { } System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张"); tickets--; } //释放锁 lock.unlock(); } }
7. 生产者与消费者例题
创建2个线程,一个线程表示生产者,另一个线程表示消费者
/** * 定义资源对象 * 成员 : 产生商品的计数器 * 标志位 */ public class Resource { int count ; boolean flag ; }
/** * 生产者线程 * 资源对象中的变量++ */ public class Produce implements Runnable{ private Resource r ; public Produce(Resource r) { this.r = r; } @Override public void run() { while (true){ synchronized (r) { //判断标志位,是否允许生产 //flag是true,生产完成,等待消费 if (r.flag ) //无限等待 try{ r.wait(); }catch (Exception ex){} r.count++; System.out.println("生产第" + r.count + "个"); //修改标志位,已经生产了,需要消费 r.flag = true; //唤醒消费者线程 r.notify(); } } } }
/** * 消费者线程 * 资源对象中的变量输出打印 */ public class Customer implements Runnable{ private Resource r ; public Customer(Resource r) { this.r = r; } @Override public void run() { while (true){ synchronized (r) { //是否要消费,判断标志位 ,允许消费才能执行 if (!r.flag ) //消费完成,不能再次消费,等待生产 try{r.wait();}catch (Exception ex){} System.out.println("消费第" + r.count); //消费完成后,修改标志位,变成已经消费 r.flag = false; //唤醒生产线程 r.notify(); } } } }
public static void main(String[] args) { Resource r = new Resource(); //接口实现类,生产的,消费的 Produce produce = new Produce(r); Customer customer = new Customer(r); //创建线程 new Thread(produce).start(); new Thread(customer).start(); }
- 线程通信的方法 wait() notify()
- 方法的调用必须写在同步中
- 调用者必须是作为锁的对象
- wait(),notify()为什么要定义在Object类
- 同步中的锁,是任意对象,任何类都继承Object
结果如下:
改为使用同步方法实现该功能
public class Resource { int count; boolean flag = false; public synchronized void getCustomer() { while (true) { if (!this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.println("消费者消费了第:" + this.count); this.flag = false; } this.notify(); } } public synchronized void getProduce() { while (true) { if (this.flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { this.count++; System.out.println("生产者生产了第:" + this.count + "个"); flag = true; } this.notify(); } } }
public class Customer implements Runnable { private Resource r; public Customer(Resource r) { this.r = r; } @Override public void run() { r.getCustomer(); } }
public class Produce implements Runnable { private Resource r; public Produce(Resource r) { this.r = r; } @Override public void run() { r.getProduce(); } }
7.1 安全问题产生
- 线程本身就是一个新创建的方法栈内存 (CPU进来读取数据)
- 线程的notify(),唤醒第一个等待的线程
- 解决办法:全部唤醒用 notifyAll() 方法
- 被唤醒线程,已经进行过if判断,一旦醒来继续执行
- 线程被唤醒后,不能立刻就执行,再次判断标志位,利用循环
- while(标志位) 标志位是true,永远也出不去
public class Resource { int count; boolean flag = false; public synchronized void getCustomer(){ while(!flag) try{ this.wait(); }catch (Exception ex){ ex.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"消费者消费了第:" + this.count); this.flag = false; this.notifyAll(); } public synchronized void getProduce(){ while(flag) try{ this.wait(); }catch (Exception ex){ ex.printStackTrace(); } this.count++; System.out.println(Thread.currentThread().getName()+"生产者生产了第:" + this.count + "个"); this.flag = true; this.notifyAll(); }
public class Produce implements Runnable { private Resource r; public Produce(Resource r) { this.r = r; } @Override public void run() { while (true) { r.getProduce(); } } }
public class Customer implements Runnable { private Resource r; public Customer(Resource r) { this.r = r; } @Override public void run() { while(true){ r.getCustomer(); } } }
public class Test { public static void main(String[] args) { Resource resource = new Resource(); Customer customer = new Customer(resource); Produce produce = new Produce(resource); new Thread(customer).start(); new Thread(customer).start(); new Thread(customer).start(); new Thread(produce).start(); new Thread(produce).start(); new Thread(produce).start(); } }
结果如下:
7.2 线程方法sleep和wait的区别
- sleep在休眠的过程中,同步锁不会丢失,不释放
- wait()等待的时候,发布监视器的所属权,释放锁。唤醒后要重新获取锁,才能执行
7.3 生产者和消费者案例的性能问题
wait()方法和notify()方法,本地方法调用OS的功能,和操作系统交互,JVM找OS,把线程停止。频繁等待与唤醒,导致JVM和OS交互的次数过多。
notifyAll()唤醒全部的线程,也浪费线程资源,为了一个线程,不得以唤醒的了全部的线程。
7.4 Lock接口深入
Lock接口替换了同步synchronized,提供了更加灵活,性能更好的锁定操作
- Lock接口中方法:newCondition() 方法的返回值是接口:Condition
- 用集合的方式去管理线程
7.5 生产者与消费者改进薇Lock接口
- Condition接口 (线程的阻塞队列)
- 进入队列的线程,释放锁
- 出去队列的线程,再次的获取锁
- 接口的方法:await() 线程释放锁,进入队列
- 接口的方法:signal() 线程出去队列,再次获取锁,此方法是唤醒一个线程
线程的阻塞队列,依赖Lock接口创建
/** * 改进为高性能的Lock接口和线程的阻塞队列 */ public class Resource { private int count ; private boolean flag ; private Lock lock = new ReentrantLock();//Lock接口实现类对象 //Lock接口锁,创建出2个线程的阻塞队列 private Condition prod = lock.newCondition();//生产者线程阻塞队列 private Condition cust = lock.newCondition();//消费者线程阻塞队列 //消费者调用 public void getCount() { lock.lock();//获取锁 //flag是false,消费完成,等待生产 while (!flag) //无限等待,消费线程等待,执行到这里的线程,释放锁,进入到消费者的阻塞队列 try{cust.await();}catch (Exception ex){} System.out.println("消费第"+count); //修改标志位,为消费完成 flag = false; //唤醒生产线程队列中的一个 prod.signal(); lock.unlock();//释放锁 } //生产者调用 public void setCount() { lock.lock();//获取锁 //flag是true,生产完成,等待消费 while (flag) //无限等待,释放锁,进入到生产线程队列 try{prod.await();}catch (Exception ex){} count++; System.out.println("生产第"+count+"个"); //修改标志位,为生产完成 flag = true; //唤醒消费者线程阻塞队列中年的一个 cust.signal(); lock.unlock();//释放锁 } }
7.6 Lock锁的实现原理
使用技术不开源,技术的名称叫做轻量级锁
使用的是CAS锁 (Compare And Swap) 自旋锁
JDK限制:当竞争的线程大于等于10,或者单个线程自旋超过10次的时候
JDK强制CAS锁取消,升级为重量级锁 (OS锁定CPU和内存的通信总线)
8. 单例设计模式
设计模式:不是技术,是以前的人开发人员,为了解决某些问题实现的写代码的经验。
所有的设计模式核心的技术,就是面向对象。
Java的设计模式有23种,分为3个类别,创建型、行为型和功能型三类。
8.1 单例模式
要求:保证一个类的对象在内存中的唯一性
第一种为饿汉式
- 私有修饰构造方法
- 自己创建自己的对象
- 方法get,返回本类对象
/** * - 私有修饰构造方法 * - 自己创建自己的对象 * - 方法get,返回本类对象 */ public class Single { private Single(){} //饿汉式 private static Single s = new Single(); // 自己创建自己的对象 // 方法get,返回本类对象 public static Single getInstance(){ return s; } }
public static void main(String[] args) { //静态方法,获取Single类的对象 Single instance = Single.getInstance(); System.out.println("instance = " + instance); }
第二种为懒汉式
- 私有修饰构造方法
- 创建本类的成员变量,不new对象
- 方法get,返回本类对象
/** * - 私有修饰构造方法 * - 创建本类的成员变量, 不new对象 * - 方法get,返回本类对象 */ public class Single { private Single(){} //懒汉,对象的延迟加载 private static Single s = null; public static Single getInstance(){ //判断变量s,是null就创建 if (s == null) { s = new Single(); } return s; } }
8.2 懒汉式的安全问题
注:一个线程判断完变量 s=null,还没有执行new对象,被另一个线程抢到CPU资源,同时有2个线程都进行判断变量,对象创建多次。
public static Single getInstance(){ synchronized (Single.class) { //判断变量s,是null就创建 if (s == null) { s = new Single(); } } return s; }
性能问题:第一个线程获取锁,创建对象,返回对象。第二个线程调用方法的时候,变量s已经有对象了,根本就不需要在进同步,不要在判断空,直接return才是最高效的。双重的if判断,提高效率 Double Check Lock。
private static volatile Single s = null; public static Single getInstance(){ //再次判断变量,提高效率 if(s == null) { synchronized (Single.class) { //判断变量s,是null就创建 if (s == null) { s = new Single(); } } } return s; }
8.2 关键字 volatile
成员变量修饰符,不能修饰其它内容。
- 关键字作用 :
- 保证被修饰的变量,在线程中的可见性
- 防止指令重排序
- 单例的模式,不使用该关键字,可能线程会拿到一个尚未初始化完成的对象(半初始化)
如果我将以下代码中的volatile关键字去掉,程序就不会结束,陷入死循环。因为线程与线程之前的变量是不可见的,一个线程修改了变量,另一个线程是看不到。所以如以下例子,如果没有volatile关键字,main线程即使修改了flag变量,MyRunnable线程也是看不到的修改信息的。
public class MyRunnable implements Runnable { private volatile boolean flag = true; @Override public void run() { m(); } private void m(){ System.out.println("开始执行"); while (flag){ } System.out.println("结束执行"); } public void setFlag(boolean flag) { this.flag = flag; } }
public class MyRunnable implements Runnable { private volatile boolean flag = true; @Override public void run() { m(); } private void m(){ System.out.println("开始执行"); while (flag){ } System.out.println("结束执行"); } public void setFlag(boolean flag) { this.flag = flag; } }
9. 线程池ThreadPool
线程的缓冲池,目的就是提高效率。new Thread().start(),线程是内存中的一个独立的方法栈区,JVM没有能力开辟内存空间,和OS交互,会影响程序的速度。
9.1 Executors 类
- 静态方法static newFixedThreadPool(int 线程的个数)
- 方法的返回值ExecutorService接口的实现类,管理池子里面的线程
- ExecutorService接口的方法
- submit (Runnable r)提交线程执行的任务(此方法会去线程池拿线程,然后调用该线程的start方法)
9.2 Callable 接口
实现多线程的程序:接口特点是有返回值,可以抛出异常 (Runnable没有)
抽象的方法只有一个 call()
启动线程,线程调用重写方法call()
- ExecutorService接口的方法
- submit (Callable c)提交线程执行的任务
- Future submit()方法提交线程任务后,方法有个返回值 Future接口类型
- Future接口,获取到线程执行后的返回值结果
public class MyCall implements Callable<String> { public String call() throws Exception{ return "返回字符串"; } }
public static void main(String[] args) throws ExecutionException, InterruptedException { //创建线程池,线程的个数是2个 ExecutorService es = Executors.newFixedThreadPool(2); //线程池管理对象service,调用方法啊submit提交线程的任务 //提交线程任务,使用Callable接口实现类 Future<String> future = es.submit(new MyCall());//返回接口类型 Future //接口的方法get,获取线程的返回值 String str = future.get(); System.out.println("str = " + str); // es.submit(my); // es.submit(my); // es.submit(my); // es.shutdown();//销毁线程池 }
10. ConcurrentHashMap(需进一步了解)
ConcurrentHashMap类本质上Map集合,键值对的集合。使用方式和HashMap没有区别。
凡是对于此Map集合的操作,不去修改里面的元素,不会锁定。
11. 线程的状态图——生命周期
在某一个时刻,线程只能处于其中的一种状态。这种线程的状态反应的是JVM中的线程状态和OS无关。