线程对象是可以产生线程的对象。比如在Java平台中Thread对象,Runnable对象。线程,是指正在执行的一个指点令序列。在java平台上是指从一个线程对象的start()开始,运行run方法体中的那一段相对独立的过程。相比于多进程,多线程的优势有:
(1)进程之间不能共享数据,线程可以;
(2)系统创建进程需要为该进程重新分配系统资源,故创 建线程代价比较小;
(3)Java语言内置了多线程功能支持,简化了java多线程编程。
一、创建线程和启动
创建线程有三种方式;
1. 实现Runable接口,实现接口,相比方式thread,基本相同,但是不同的是,我们一般遵守只有在真的使用某个类的时候,才会对其进行修改。
2.创建Thread对象,其中Thread的参数是1中实现的对象
3.调用Thread.start()
class testThread implements Runnable{ } public class Test{ public static void main(String []args){ testThread worker= new testThread(); Thread thread = new Thread(worker); thread.start(); } } // 另外一种灵活的方式: Thread thread = new Thread(new Runnable() { }); thread.start(); |
方式2 : 继承Thread 类
1.继承Thread类
2.重写run
3.Thread.start()
方式3 :带有返回值的实现方式- 实现callable接口,重写call()方法
1. callable接口会在结束,返回一个执行结果
2. callable接口在执行的时候可以抛出异常
3. 运行call()可以拿到future对象,future对象表示异步计算是否完成。当调用futrue的get方法时,当前线程会阻塞直到call()执行完成。
class TestCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { // TODO Auto-generated method stub Thread.sleep(2000); return 1; } } main 方法实现 : ExecutorService excutor = Executors.newSingleThreadExecutor(); System.out.println("end time :" + System.currentTimeMillis()); 输出: start time :1520738016219 |
二.线程的生命周期:
其中体现一点,start() 和run() 的区别
Start的作用是启动一个新线程。通过start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法,run方法运行结束,此线程随即终止。
start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。
run()就和普通的成员方法一样,可以被重复调用。
如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。
三。线程的各种管理方式
1. 线程休眠 - Thread.sleep(1000)
<public static native void sleep(long millis) throws InterruptedException;>
我们可以看到,sleep是一个静态的本地方法,所以调用他的时候,直接使用Thread.sleep即可。
当调用之后,代码所在的当前线程进入阻塞状态,当时间到了之后,线程唤醒,但是线程不一定立即去执行,依赖于当前的CPU时间片,也就是说sleep的时间一般会大于参数设置的时间
一个常见的问题 sleep和wait的区别 : 1、sleep()方法是Thread的方法,作用是正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(sleep方法只让出了CPU,而并不会释放同步资源锁!!!); wait()方法,是Obeject的方法,作用是当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度); 2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用; |
让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能.
和Sleep的区别 (从上面的介绍也可以看出来)
<1>.sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
<2>sleep方法声明抛出InterruptedException,调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
3. 线程合并——join作用 :几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。
常见的用例,三个线程有序的进行执行。
4.设置线程的优先级
Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~10之间,也可以使用Thread类提供的三个静态常量:MAX_PRIORITY =10 MIN_PRIORITY =1 NORM_PRIORITY =5
需要注意的是 线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
5. 终止线程的几种方式
方式1 :线程执行完成,自行终止,这是一般的情况
方式2: 通过判断条件来进行线程的退出,实际上也是在run方法中设置run方法执行完成的条件
四. 线程的同步 - 重点
实现线程同步有以下几种方式 :
方式1 :使用synchronized关键字
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
下面分别介绍
1.1 修饰方法
当修饰普通方法的时候,持有的是当前声明对象的锁,也就是说,如果同时声明了两个对象,那么这两个对象是互相不影响的。
当修饰静态方法的时候,持有的是类的锁,和声明类的对象没有关系。
验证如下 :-- 注意如果使用代码测试过程中,不能把所有的case同时测试,因为线程的具体执行时间不是start后立即执行,所以验证结果可能会不准确
class TestSync{ public synchronized static void func1(){ System.out.println("this is synchronized static func" +System.currentTimeMillis()); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public synchronized void func2(){ System.out.println("this is synchronized common func" + System.currentTimeMillis()); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 结果 : case-1 : System.out.println("test synchronized static :"); new Thread(){public void run() {TestSync.func1();}}.start(); new Thread(){public void run() {TestSync.func1();}}.start(); out-1 : test synchronized static : this is synchronized static func1520741647132 this is synchronized static func1520741649132 case-2 : System.out.println("test synchronized common with 1 object :"); final TestSync test1 = new TestSync(); new Thread(){public void run() {test1.func2();}}.start(); new Thread(){public void run() {test1.func2();}}.start(); out-2 : test synchronized common with 1 object : this is synchronized common func1520744340943 this is synchronized common func1520744342944 case-3 : System.out.println("test synchronized common with 2 object :"); final TestSync test1 = new TestSync(); final TestSync test2 = new TestSync(); new Thread(){public void run() {test1.func2();}}.start(); new Thread(){public void run() {test2.func2();}}.start(); out-3 : test synchronized common with 1 object : this is synchronized common func1520744415515 this is synchronized common func1520744417515 |
1.2 修饰代码块
修饰代码块的时候,在synchronized的()中会填写持有的对象,对象一般有,普通对象,静态对象。
验证如下 :
public void func3(){ synchronized (obj1) { System.out.println("this is hold a common object" + System.currentTimeMillis()); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public void func4(){ synchronized (obj2) { System.out.println("this is hold a static object" + System.currentTimeMillis()); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } case -1 System.out.println("test synchronized same class object hold a common object :"); new Thread(){public void run() {test1.func3();}}.start(); out-1 没有悬念 - 因为是同一个对象,所以同步 test synchronized same class object hold a common object : this is hold a common object1520745143077 case -2 System.out.println("test synchronized 2 class object hold a common object :"); new Thread(){public void run() {test2.func3();}}.start(); out-2 没有悬念,两个类对象,分别有自己的锁对象,所以不同步 test synchronized 2 class object hold a common object : this is hold a common object1520745272619 case -3 System.out.println("test synchronized 2 class object hold a static object :"); new Thread(){public void run() {test2.func4();}}.start(); out-3 静态变量属于类,而不属于创建的类的对象,所以同步有效 test synchronized 2 class object hold a static object : |
2. 使用特殊域变量(volatile)实现线程同步
2.1 volatile关键字为域变量的访问提供了一种免锁机制;
2.2 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;每次使用该域就要重新计算,而不是使用寄存器中的值;
2.3 volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
使用volatile的一个场景是单例模式中,具体的可以自行百度或者谷歌
3. 使用重入锁(Lock)实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
使用方式 :
private ReentrantLock lock = new ReentrantLock(); public void func5(){ lock.lock();System.out.println("this is synchronized static func" +System.currentTimeMillis()); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } lock.unlock(); } |
五. 线程间通信-> 主要是通过线程间的通信进行线程执行的控制,注意和Android线程间通信进行区别
1. 使用wait(),notify(),notifyAll()
线程执行wait()后,放弃当前持有的锁,当其他线程调用notify之后,再次进入就绪状态,等待CPU的调度。
需要注意的是冻结的线程是保存在内存的线程池中的,调用notify(),是按照保存的冻结线程的顺序进行唤醒的,notifyAll唤醒所有线程.
另外,这三个方法都是基于当前持有的对象锁的,也就是说,操作的对象都是同一个才有作用。
因为我们一般使用锁对象都是普通的对象,所以可能出现在代码中,不同的类对象持有不同的锁,这样多个线程的
2.使用Condition控制线程通信
jdk1.5中,提供了多线程的升级解决方案为:
(1)将同步synchronized替换为显式的Lock操作;
(2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;
(3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。
3、使用阻塞队列(BlockingQueue)控制线程通信
-> 这个平时自己使用的比较少copy from : https://www.cnblogs.com/snow-flower/p/6114765.html
BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
BlockingQueue提供如下两个支持阻塞的方法:
(1)put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
(2)take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:
(1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
(2)在队列头部删除并返回删除的元素。包括remove(),poll(),take(),当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
(3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。
BlockingQueue接口包含如下5个实现类:
ArrayBlockingQueue :基于数组实现的BlockingQueue队列。
LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),DelayQueue根据集合元素的getDalay()方法的返回值进行排序。
六、线程池
引入线程池主要是因为下面几个原因:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,用线程池进行统一的分配,调优和监控
6.1 常见的线程池
6.2 线程池的创建和逻辑
AVA语言为我们提供了两种基础线程池的选择:ScheduledThreadPoolExecutor和ThreadPoolExecutor。它们都实现了ExecutorService接口。
创建线程池的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
● corePoolSize:线程池主要用于执行任务的是“核心线程”,“核心线程”的数量是你创建线程时所设置的corePoolSize参数决定的。如果不进行特别的设定,线程池中始终会保持corePoolSize数量的线程数(不包括创建阶段)。
● maximumPoolSize参数也是当前线程池允许创建的最大线程数量。那么如果设置的corePoolSize参数和设置的maximumPoolSize参数一致时,线程池在任何情况下都不会回收空闲线程。keepAliveTime和timeUnit也就失去了意义。
● keepAliveTime参数和timeUnit参数也是配合使用的。keepAliveTime参数指明等待时间的量化值,
● timeUnit指明量化值单位。例如keepAliveTime=1,timeUnit为TimeUnit.MINUTES,代表空闲线程的回收阀值为1分钟。
● BlockingQueue:线程池使用的缓冲队列,也就是当线程提交之后,core和临时线程都在使用,那么进行缓存队列
● threadFactory : 新建线程工厂
● RejectedExecutionHandler : 拒绝策略 ; 如果这个任务,无法被“核心线程”直接执行,又无法加入等待队列,又无法创建“非核心线程”直接执行,且你没有为线程池设置RejectedExecutionHandler。这时线程池会抛出RejectedExecutionException异常,即线程池拒绝接受这个任务。(实际上抛出RejectedExecutionException异常的操作,是ThreadPoolExecutor线程池中一个默认的RejectedExecutionHandler实现:AbortPolicy,这在后文会提到)
网上的一张图很好的解释了线程池的结构:
线程池的逻辑如下 :
1、首先通过线程池提供的submit()方法或者execute()方法,要求线程池执行某个任务。线程池收到这个要求执行的任务后,会有几种处理情况:
1.1、如果当前线程池中运行的线程数量还没有达到corePoolSize大小时,线程池会创建一个新的线程,无论已经创建的线程是否处于空闲状态。
1.2、如果当前线程池中运行的线程数量已经达到设置的corePoolSize大小,线程池会把这个任务加入到等待队列中。直到某一个的线程空闲了,线程池会根据设置的等待队列规则,从队列中取出一个新的任务执行。
1.3、如果根据队列规则,这个任务无法加入等待队列。这时线程池就会创建一个“非核心线程”直接运行这个任务。注意,如果这种情况下任务执行成功,那么当前线程池中的线程数量一定大于corePoolSize。
1.4、如果这个任务,无法被“核心线程”直接执行,又无法加入等待队列,又无法创建“非核心线程”直接执行,且你没有为线程池设置RejectedExecutionHandler。这时线程池会抛出RejectedExecutionException异常,即线程池拒绝接受这个任务,抛出RejectedExecutionException异常的操作
2、 一旦线程池中某个线程完成了任务的执行,它就会试图到任务等待队列中拿去下一个等待任务(所有的等待任务都实现了BlockingQueue接口,按照接口字面上的理解,这是一个可阻塞的队列接口),它会调用等待队列的poll()方法,并停留在哪里。3、当线程池中的线程超过你设置的corePoolSize参数,说明当前线程池中有所谓的“非核心线程”。那么当某个线程处理完任务后,如果等待keepAliveTime时间后仍然没有新的任务分配给它,那么这个线程将会被回收。线程池回收线程时,对所谓的“核心线程”和“非核心线程”是一视同仁的,直到线程池中线程的数量等于你设置的corePoolSize参数时,回收过程才会停止。
6.3 线程池的使用->用 newSingleThreadExecutor 来举例
ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub System.out.println("test"); } }); Future<String> future = executor.submit(new Callable<String>() { @Override public String call() throws Exception { // TODO Auto-generated method stub return "testSubmit"; } }); try { System.out.println(future.get()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } executor.shutdown(); // 使用完成,一定要关闭线程池,否则会一直存在,造成内存泄漏 |
七. 死锁
7.1 死锁的条件
互斥条件:资源不能被共享,只能被同一个进程使用
请求与保持条件:已经得到资源的进程可以申请新的资源
非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺
循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源
7.2 处理死锁的方法
1. 预防死锁 ->大多数情况下,在使用请求资源的时候,通过逻辑的控制,尽量预防死锁的发生
常用的破坏死锁条件的方式:
破坏请求和保持条件
协议1
破坏不可抢占条件
破坏循环等待条件
2. 避免死锁 和预防死锁的区别就是,在资源动态分配过程中,用某种方式防止系统进入不安全的状态,比如在请求资源的时候进行资源状态的判断等
3.检测死锁 运行时出现死锁,能及时发现死锁,把程序解脱出来
4.解除死锁 发生死锁后,解脱进程,通常撤销进程,回收资源,再分配给正处于阻塞状态的进程。
扩展内容:
如何控制某个方法允许并发访问线程的个数?
构造函数创建了一个 Semaphore 对象,并且初始化了 5 个信号。 这样的效果是控件 test 方法最多只能有 5 个线程并发访问,对于 5 个线程时就排队等待,走一个来一下; 请求一个信号(消费一个信号),如果信号被用完了则等待; 释放一个信号,释放的信号新的线程就可以使用了. |
ReentrantLock 、synchronized和volatile比较
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现; 2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁; 3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断; 4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。 5)Lock可以提高多个线程进行读操作的效率。 而对于volatile ,实现的是一种共享变量的概念。(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 2)禁止进行指令重排序。 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 有序性:即程序执行的顺序按照代码的先后顺序执行。 |
ReentrantLock的内部实现:
以上是经常使用的一些多线程并发的概念。如有不准确的地方,还请指正。