一、线程与进程
-
什么是进程,什么是线程?
进程: 是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程: 是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
一个程序至少一个进程,一个进程至少一个线程;
一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。 -
进程线程的区别
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
资源拥有:同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的。
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程是处理器调度的基本单位,但是进程不是。
两者均可并发执行。
-
优缺点
线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。
-
什么地方会用到多线程?
后台线程:比如定期执行一些特殊任务,如定期更新配置文件,任务调度,一些监控用于定期信息采集等。
最典型的应用比如tomcat,tomcat内部采用的就是多线程,上百个客户端访问同一个web应用,tomcat接入后都是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用到我们的servlet程序,比如doGet或者doPost方法。还有就是需要异步处理的时候,需要使用多线程。
特别耗时的操作,如备份数据库,可以开个线程执行备份,然后执行返回,前台不断向后台询问线程执行状态。
二、 线程类 Thread 详解
1、线程的生命周期与实现
-
线程的生命周期
-
多线程的实现
方式一:继承Thread并重写run方法.
/* * 启动线程要执行start而不是直接调用run方法 * 当start方法调用完毕后,该线程纳入到线程调度, * 等待分配CPU时间,一旦分配后该线程的run方法 * 会自动被调用开始执行任务。 * * 线程对线程调度的工作是不可控的 * 时间片长短不可控,分配给那个线程也不可控 * 线程调度会尽可能均匀的将时间片次数分配 * 分配给不同的线程,但绝不保证一个线程一次 * 这样的切换方式,线程也没有能力主动获取CPU的时间片。 */ public class TreadDemo1 { public static void main(String[] args) { Thread t1 = new Mythread1(); Thread t2 = new Mythread2(); t1.start(); t2.start(); } } /** * 第一种创建线程的方式有两个设计上的不足: * 1:由于java是单继承的,这就导致一个类若继承了 * 线程就不能继承其他类,在实际开发中会出现继 * 承冲突问题。 * 2:由于重写run方法定义了线程要执行的任务, * 就导致线程与其执行的任务有一个强耦合关系 * 不利于线程重用。 */ class Mythread1 extends Thread{ public void run (){ for(int i=0;i<1000;i++){ System.out.println("你是谁啊?"); } } } class Mythread2 extends Thread{ public void run(){ for(int i=0;i<1000;i++){ System.out.println("我是查水表的!"); } } }
方式二:单独定义线程任务:实现Runnable接口
public class ThreadDemo2 { public static void main(String[] args) { Runnable r1 = new MyRunnable1(); Runnable r2 = new MyRunnable2(); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } } class MyRunnable1 implements Runnable{ public void run() { for(int i=0;i<1000;i++){ System.out.println("你是谁?"); } } } class MyRunnable2 implements Runnable{ public void run(){ for(int i=0;i<1000;i++){ System.out.println("我是查水表的"); } } } //使用匿名内部类创建线程: public class Test { public static void main(String[] args) { new Thread(){ public void run(){ for(int i =0;i<1000;i++){ System.out.println("你是谁?"); } } }.start(); new Thread(new Runnable(){ public void run() { for(int i=0;i<1000;i++){ System.out.println("我是查水表的"); } } }).start(); } }
方法三、实现 Callable 接口,重写 call 方法
Callable 接口实际是属于 Executor 框架中的功能类,Callable 接口与 Runnable 接口的功能类似,但提供了比 Runnable 更强大的功能,主要表现为以下 3 点:- Callable 可以在任务结束后提供一个返回值,Runnable 无法提供这个功能
- Callable 中的 call 方法可以抛出异常,而 Runnable 的 run 方法不能抛出异常
- 运行 Callable 可以拿到一个 Future 对象,Future 对象表示异步计算的结果,它提供了检查计算是否完成的方法,由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用 Future 来监视目标线程调用 call() 方法的情况,当调用 Future 的 get() 方法以获取结果时,当前线程就会阻塞以,直到 call() 方法结束返回结果
public class CallableAndFuture { /** * 创建线程类 */ public static class CallableTest implements Callable<String> { @Override public String call() throws Exception { return "Helle World"; } } public static void main(String[] args){ ExecutorService executorService = Executors.newSingleThreadExecutor(); //启动线程 Future<String> future = executorService.submit(new CallableTest()); //等待多线程结束,并获取返回结果 try { System.out.println("waiting thread to finish"); System.out.println(future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
-
线程池
-
线程池的作用
(1) 控制线程数量; (2)重用线程; (3)当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。
-
线程池的概念
(1) 首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出1个空闲的线程为之服务,服务完后不关闭该线程而是将该线程还回到线程池中。
(2) 在线程池的编程模式下,任务是提交给整个线程池,而
不是直接交给某个线程,线程池在拿到任务后,它就在
内部找有无空闲的线程,再把任务交给内部某个空闲的
线程。
(3) 一个线程同时只能执行一个任务,但可以同时向一个线
程池提交多个任务 -
使用 ExecutorService 实现线程池有以下几种实现策略:
• Executors.newCachedThreadPool()
创建一个可根据需要创建新线程的线程池,但是在以
前构造的线程可用时将重用它们。
• Executors.newFixedThreadPool(int nThreads)
创建一个可重用固定线程集合的线程池,以共享的无
界队列方式来运行这些线程。
• Executors.newScheduledThreadPool(int
corePoolSize)
创建一个线程池,它可安排在给定延迟后运行命令或
者定期地执行。public class ThreadPool { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(2); for(int i=0;i<5;i++){ Runnable runn = new Runnable() { public void run() { Thread t = Thread.currentThread(); System.out.println(t+"正在执行任务"); try { Thread.sleep(5000); System.out.println(t+"运行任务结束"); } catch (InterruptedException e) { System.out.println("线程被中断了!!!!"); } } }; threadPool.execute(runn); } threadPool.shutdown(); //等待线程池手头工作完成后停止线程池 //threadPool.shutdownNow();//立刻停止线程池 System.out.println("停止线程池!"); } }
-
2、Thread类常用API
-
线程优先级:setPriority()方法。
线程的优先级有10个等级,对应10个整数,分别是1-10
其中1是最低优先级,10是最高优先级,5为默认优先级。
有三个常量对应:
MIN_PRIORITY,MAX_PRIORITY;NORM_PRIORITY。
理论上线程优先级越高的线程获取的CPU时间片次数越多。public class Thread_priority { public static void main(String[] args) { Thread max = new Thread(){ public void run(){ for(int i=0;i<1000;i++){ System.out.println("max"); } } }; Thread min = new Thread(){ public void run(){ for(int i=0;i<1000;i++){ System.out.println("min"); } } }; Thread nor = new Thread(){ public void run(){ System.out.println("nor"); } }; new Thread(new Runnable(){ public void run() { for(int i=0;i<1000;i++){ System.out.println("hahahahahah"); } } }).start() ; min.setPriority(1); max.setPriority(10); min.start(); max.start(); nor.start(); } }
-
线程Thread类的几个静态方法
---------static void sleep(long ms)--------- / * * 该方法会让运行该方法的线程进入阻塞状态指定毫秒时间 * 当超时后线程会自动回到runnable状态,等待再次运行。 * 通常使用sleep做间隔时间等操作。 */ public class Thread_sleep { public static void main(String[] args) { /* * 电子表,每秒输出当前系统时间 * 如:15:20:12 */ SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); while(true){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(sdf.format(new Date())); } } } -------static Thread currentThread()--------- /* * 该方法可以获取运行该方法的线程实例引用 */ public class Thread_currentThread { public static void main(String[] args) { //获取了运行main方法的线程 Thread main = Thread.currentThread(); System.out.println("运行main方法的线程:"+main); dosome(); new Thread(){ public void run(){ Thread t = Thread.currentThread(); System.out.println("自定义线程:"+t); dosome(); } }.start(); } public static void dosome(){ Thread t = Thread.currentThread(); System.out.println("运行dosome的线程是:"+t); } } --------static void yield()--------- * 该方法用于使当前线程主动让出当次CPU * 时间片回到Runnable状态,等待分配时间片
-
获取线程信息的相关方法:
public class Thread_info { public static void main(String[] args) { Thread main = Thread.currentThread(); //1. 获取线程标示符ID long id = main.getId(); System.out.println("id:"+id); //2. 获取线程名字 String name = main.getName(); System.out.println("name:"+name); //3. 查看线程优先级 int priority = main.getPriority(); System.out.println("优先级prioity:"+priority); //4. 查看线程是否处于活动线程 boolean isAlive = main.isAlive(); System.out.println("isAlive:"+isAlive); //5. 查看线程是否为守护线程 boolean isDaemon = main.isDaemon(); System.out.println("isDaemon:"+isDaemon); //6. 查看线程是否已经中断 boolean isInterrupted = main.isInterrupted(); System.out.println("isInterruped:"+isInterrupted); } }
-
守护线程:用 setDaemon(true) 方法设置
守护线程又称后台线程
与普通线程的一个最主要的区别在于结束时机;
当一个进程结束时,所有运行的守护线程都会被强制结束。
当一个进程中的所有前台(普通线程)线程结束时,进程就会结束。public class Thread_daemon { public static void main(String[] args) { /* * 前台线程:rose */ Thread rose = new Thread(){ public void run(){ for(int i=0;i<5;i++){ System.out.println("rose: let me go!"); try { Thread.sleep(1000); } catch (InterruptedException e) { } } System.out.println("rose: 啊啊啊啊啊AAAAaaaa....."); System.out.println("音效:噗通!"); } }; /* * 后台线程:Jack */ Thread jack = new Thread(){ public void run(){ while(true){ System.out.println("jack: you jump,i jump!"); try { Thread.sleep(1000); } catch (InterruptedException e) { } } } }; /* * 设置后台线程必须在start方法调用前执行 */ jack.setDaemon(true); rose.start(); jack.start(); System.out.println("main线程结束了!"); } }
-
线程提供的join()方法允许使线程间同步运行:
/* * 同步:有先后顺序的执行 * 异步:格执行各的 */ public class Thread_join { //图片是否下载完毕 public static boolean isFinsh = false; public static void main(String[] args) { //注意:当一个局部内部类调用局部变量时,该变量必须是final修饰的(jdk1.8以前) final Thread download = new Thread(){ public void run(){ System.out.println("down: 图片开始下载....."); for(int i=0;i<=100;i++){ System.out.println("down:"+i+"%"); try { Thread.sleep(50); } catch (InterruptedException e) { } } System.out.println("down:图片下载完毕!"); isFinsh = true; } }; Thread show = new Thread(){ public void run(){ System.out.println("show:开始显示图片...."); //等待下载线程将图片下载完毕 try { //注意: 在那个线程中使用join()会阻塞谁,谁调用就是该线程等待谁结束 download.join(); } catch (InterruptedException e) { e.printStackTrace(); } if(!isFinsh){ throw new RuntimeException("show:图片加载失败!"); } System.out.println("show:图片显示完毕!"); } }; download.start(); show.start(); } }
三、线程的同步
1、锁定分类
-
可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
-
可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
例如: lockInterruptibly() 的用法可体现了Lock的可中断性。 -
公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁 (通过构造器传入 true)。
-
读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。
2、加锁 释放锁的原理
1.JVM字节码反编译为汇编指令查看 Monditorenter 和 Monditorexit 指令。
2. 可重入原理:加锁次数计数器
- JVM负责跟踪对象被加锁的次数
- 线程第一次给对象加锁的时候,计数变为1 。每当像个相同的线程在次对象上再次获得锁,计数器递增。
- 当任务离开时,计数递减,当计数为0的时候,锁完全释放
3、wait() 方法与 sleep() 方法
- sleep()方法,属于Thread类中的。而wait()方法,则是属于Object类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
- 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有其他线程针对此对象调用notify()方法或notifyAll()后本线程才进入对象锁定池准备获取对象锁进入运行状态。
- notify 和 notifyAll的区别:
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
4、 synchronized 关键字详解
1. Synchronized 的两种用法
-
对象锁(锁的范围为一个对象的实例,不同实例对象拥有不同的锁),包括方法锁(默认锁对象为this当前实例对象) 和 同步代码块锁(自己指定锁对象)
-
代码块锁(粒度较小,推荐)
public void method1() { synchronized (objMain) { System.out.println(Thread.currentThread().getName()+"_method1开始运行.."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"_method1结束.."); } }
-
普通方法锁(粒度较大)
public synchronized void method2() { System.out.println(Thread.currentThread().getName()+"_method2开始运行.."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"_method2结束.."); }
-
-
类锁(java类可以有多个对象,但只有一个class对象)锁的范围为某一个类,所有的实例共享一把锁
-
静态方法锁
public synchronized static void method3() { System.out.println(Thread.currentThread().getName()+"_method3开始运行.."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"_method3结束.."); }
-
代码块类锁
public void method1() { synchronized (ClassName.Class) { System.out.println(Thread.currentThread().getName()+"_method4开始运行.."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"_method4结束.."); } }
-
2. synchronized的性质
- 可重入性质: 当一个线程得到一个对象的锁后,在该锁里执行代码的时候可以再次获得该对象的其他锁。当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。可重入锁就是自己获得自己内部的锁。如果没有重入锁的话,加入有一个线程在获取了对象A的锁之后,再次请求A的锁的时候,由于还没有释放之前获得的锁,所以这个时候就会出现死锁。可重入锁的特性还有父子可继承性。
- 不可中断: 一旦这个锁已经被别人获得了,如果我还想获得,我只能等待或者阻塞,直到别人释放这个锁,如果别人永远不释放锁,那么我只能永远等待下去。(Lock类 拥有中断能力,可以设置等待时间)
- 异常自动释放锁
- 优点:实现简单,语义清晰,便于JVM堆栈跟踪,加锁解锁过程由JVM自动控制,提供了多种优化方案,使用更广泛
- 缺点:不够灵活,加锁和释放锁单一,无法知道是否成功获取到锁。
public class Service2 {
public int i = 10;
public synchronized void mainMethod(){
i--;
System.out.println("main print i="+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Service3 extends Service2 {
public synchronized void subMethod(){
try{
while (i>0){
i--;
System.out.println("sub print i= "+i);
Thread.sleep(100);
this.mainMethod();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class MyThread extends Thread {
public void run(){
Service3 service3 = new Service3();
service3.subMethod();
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
5、 Lock类 详解
1. Lock与synchronized的区别
- Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的。
- 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的 条件下提供一种退出的机制。
- 一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以 及设置等待时限等方式退出条件队列。
- synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式
- synchronized不需要用户去手动释放锁,Lock 必须手动释放锁
- Lock 比 synchronized 更加灵活,同时使用难度更高。
2. Lock API
-
void lock(); // 尝试获取锁,获取成功则返回,否则阻塞当前线程
Lock lock = ...; lock.lock(); try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 }
-
void lockInterruptibly() throws InterruptedException; // 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
public void method() throws InterruptedException { lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); } } // lockInterruptibly()响应中断的实例: public class Test { private Lock lock = new ReentrantLock(); public static void main(String[] args) { Test test = new Test(); MyThread thread1 = new MyThread(test,"thread1"); MyThread thread2 = new MyThread(test,"thread2"); //thread1.setPriority(1); //thread2.setPriority(10); thread1.start(); thread2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); } public void insert(Thread thread) throws InterruptedException{ lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出 try { System.out.println(thread.getName()+"得到了锁"); long startTime = System.currentTimeMillis(); for( ; ;) { if(System.currentTimeMillis() - startTime >= 1000*5) break; //插入数据 } } finally { System.out.println(Thread.currentThread().getName()+"执行finally"); lock.unlock(); System.out.println(thread.getName()+"释放了锁"); } } } class MyThread extends Thread { private Test test = null; private String name = null; public MyThread(Test test, String string) { this.test = test; this.name = string; } public void run() { try { Thread.currentThread().setName( name ); test.insert(Thread.currentThread()); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+"被中断"); } } }
-
boolean tryLock(); // 尝试获取锁,获取锁成功则返回true,否则返回false
-
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
Lock lock = ...; if(lock.tryLock()) { try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 } }else { //如果不能获取锁,则直接做其他事情 }
-
void unlock(); // 释放锁
-
Condition newCondition(); //返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
3. 实现类
-
ReentrantLock(可重入锁) , 实现了Lock接口
Lock lock = new ReentrantLock(); //常用 API : isFair() //判断锁是否是公平锁 isLocked() //判断锁是否被任何线程获取了 isHeldByCurrentThread() //判断锁是否被当前线程获取了 hasQueuedThreads() //判断是否有线程在等待该锁
-
第二种 通过ReentrantReadWriteLock的静态内部类实现读写锁,实现了ReadWriteLock 接口
ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); Lock readLock = readWriteLock.readLock(); Lock writeLock = readWriteLock.writeLock(); //注意 : 一个锁是读锁,一个锁是写锁。读锁与读锁之间是共享的,读锁与写锁之间是互斥的,写锁与写锁之间也是互斥的。
4. Condition 类
任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制。
同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持
Lock lock = = new ReentrantLock();
Condition condition = lock.newCondition();
// 造成当前线程在接到信号或被中断之前一直处于等待状态。
void await()
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
boolean await(long time, TimeUnit unit)
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout)
// 造成当前线程在接到信号之前一直处于等待状态。
void awaitUninterruptibly()
// 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
boolean awaitUntil(Date deadline)
// 唤醒一个等待线程。
void signal()
// 唤醒所有等待线程。
void signalAll()
6、常用的线程安全API
-
StringBuilder不是线程安全的,StringBuffer是线程安全的。
-
对于集合和Map而言,可以使用Collections提供的静态方法对已有的集合或Map进行线程安全转换。
public class SyncAPI { public static void main(String[] args) { /* * ArrayList,LinkedList * 都不是线程安全的 */ List<String> list = new ArrayList<String>(); list.add("one"); list.add("two"); list.add("three"); list.add("four"); System.out.println(list); /* * 将给定的List集合转换为一个线程安全的List集合 */ list = Collections.synchronizedList(list); System.out.println(list); /* * HashSet不是线程安全的 */ Set<String> set = new HashSet<String>(list); System.out.println(set); /* * 将给定的Set集合转换为线程安全的Set集合 */ set = Collections.synchronizedSet(set); System.out.println(set); /* * HashMap不是线程安全的(HashSet就是利用了HashMap中的K列,Value为null) */ Map<String,Integer> map = new HashMap<String,Integer>(); map.put("语文",99); map.put("数学",100); map.put("英语", 35); System.out.println(map); /* * 将给定的Map转换为线程安全的Map */ map = Collections.synchronizedMap(map); System.out.println(map); /* * 对于集合操作而言,线程安全的集合也不对 遍历与增删互斥。 * 说以当使用迭代器遍历集合时,应自行维护与集合增删元素操作间的互斥。 */ } }
7、并发编程使用方法总结
优先使用线程安全的API开控制并发安全,然后考虑使用synchronized 来控制同步,最后考虑使用Lock来控制并发,可减少程序出现问题的几率。
四、java 内存模型
1. 计算机内存模型
-
计算机内存模型
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
-
带来的问题
如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
-
解决方法
1 通过在总线加LOCK#锁的方式
锁住总线期间,其他CPU无法访问内存,导致效率低下。
2 通过缓存一致性协议
最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
2、java 内存模型
1. java 内存模型
- 在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。
- 注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
- Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
2. 并发编程中的三个概念
-
原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
x = 10; // 具有原子性 y = x; //先读取x的值,再将y的值写入工作内存;不具有原子性 x++; //读x值到工作内存,x=x+1,x值写入主存;不具有原子性 x = x + 1; //同上,不具有原子性
-
可见性
可以通过volatile,synchronized,Lock及float可以保证内存可见性
-
实现共享变量可见性必须保证两点:
- 线程修改以后的共享变量值能够及时的从工作内存刷新到主内存中
- 其他线程能够及时把共享变量的最新值从主内存跟新到自己的工作内存中。
-
synchronized和Lock保证可见性
synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。 -
volatile 实现可见性(不具有原子性)
- 底层原理:通过加入内存屏障和禁止重排序优化来实现
- 对 volatile 变量执行写操作时,会在写操作后加一条 store 凭证指令
- 对 volatile 变量执行读操作时,会在读操作前加一条load屏障指令
- 通俗理解:
volatile 变量在每次被线程访问时,都强迫从主内存中重新读取改变量的值,而当该变量发生变化时,又会强迫线程立即将最新值刷新到主内存中。
-
volatile 适用条件
- 对变量的读写操作不依赖当前值(满足:boolean 不满足:a++ ···)
- 该变量没有包含在具有其他变量的不变式中(不满足:low ,up 都为共享变量 不变式 low < up )
-
-
有序性
- 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性。
- happens-before原则(先行发生原则): Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
- as-if-serial 语义 : 无论如何重排序,程序执行的结果应该和代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)
3. 其他介绍
- 争用条件介绍
当多个线程同时访问同一数据(内存区域)时,每个线程都尝试操作改数据,从而导致数据被破坏(corrupted),这种现象称为争用条件。
public class ThreadTest implements Runnable { public static int count = 0 ; public static void main(String[] args) { Thread thread01 = new Thread(new Contention()); Thread thread02 = new Thread(new Contention()); thread01.start(); thread02.start(); } public void run() { for(int i=0;i<10000;i++ ){ System.out.println("count:"+ (++count)); } } } Console 输出: count:19867 count:19868 count:19869 count:19870 count:19871 count:19872 count:19873