1.程序、进程、线程的区别
2. 线程的状态与生命周期
3.线程的调度与优先级
4.实现多线程的两种方法
5.如何实现线程同步(同步和互斥的区别)
6.如何实现线程之间协作(生产者-消费者模式)
7.守护线程
8.线程的常用方法
9.什么是线程池
10.死锁,如何避免
11. 高级同步机制(比synchronized更灵活的加锁机制)
12. Java中的volatile关键字是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
13.Java 内存模型是什么?
14.多线程编程的注意事项
1.程序、进程、线程的区别
2. 线程的状态与生命周期
当一个Thread类或其子类对象被声明并创建,新生的线程对象就处于新建状态(此时它已经有了内存空间和其他资源)。
线程已经创建就具备了运行的条件,一旦轮到它来享用CPU资源时,即JVM将CPU使用权切换给该线程时,此线程就可以脱离创建它的主线程独立开始自己的生命周期了。
线程创建后仅仅是占有了内存资源,在JVM管理的线程中还没有这个线程,此线程必须调用start()方法(从父类继承)通知JVM,这样JVM就知道又有一个新的线程排队等候切换。
线程挂起的原因有一下四种:
(1)、JVM将CPU资源从当前线程切换给其他线程,使本线程让出CPU的使用权,并处于挂起状态。
(2)、线程使用CPU资源期间,执行了sleep(int millsecond)方法,使当前线程进入休眠状态。sleep(int millsecond)方法是Thread类中的一个类方法,线程执行该方法就立刻让出CPU使用权,进入挂起状态。经过参数millsecond指定的毫秒数之后,该线程就重新进到线程队列中排队等待CPU资源,然后从中断处继续运行。
(3)、线程使用CPU资源期间,执行了wait()方法,使得当前线程进入等待状态。等待状态的线程不会主动进入线程队列等待CPU资源,必须由其他线程调用notify()方法通知它,才能让该线程从新进入到线程队列中排队等待CPU资源,以便从中断处继续运行。
(4)、线程使用CPU资源期间,执行某个操作进入阻塞状态,如执行读/写操作引起阻塞。进入阻塞状态时线程不能进入线程队列,调度器将忽略线程,不会分配给线程任何CPU时间,只有引起阻塞的原因消除时,进入就绪状态,便从中断处继续运行。
4、死亡:
死亡状态就是线程释放了实体,即释放了分配给线程对象的内存。线程死亡的原因有两个:
(1)、正常运行的线程完成了它的全部工作,即执行完run()方法中的全部语句,结束了run()方法。
(2)、线程被提前强制性终止,即强制run()方法结束。stop方法 interrupt方法
3.线程的调度与优先级
实际编程时,不提倡使用线程的优先级来保证算法的正确执行。
4.实现多线程的两种方法
java中实现多线程操作有两种方法:继承Thread类和实现Runnable接口
一、继承Thread类 //继承Thread类 class MyThread extends Thread { private String name ; public MyThread(String name) { this.name = name; } public void run() {//覆写Thread类中的run方法 System.out.println("MyThread-->"+ name); } } public class TestThread { public static void main(String args[]) { MyThread t1 = new MyThread("线程1"); MyThread t2 = new MyThread("线程2"); t1.start();//调用线程启动方法 t2.start();//调用线程启动方法 } } 二、实现Runnable接口 class MyThread implements Runnable { private String name ; public MyThread(String name) { this.name = name; } public void run() {//覆写Thread类中的run方法,这是线程的主体 System.out.println("MyThread-->"+ name); } } public class TestThread { MyThread t = new MyThread("线程"); new Thread(t).start(); new Thread(t).start(); }
5.如何实现线程同步
在Java语言中,每个对象都有一个锁,一个线程可以通过关键字synchronized(互斥锁)来申请获取某个对象的锁,关键字synchronized可以被用于方法(粗粒度锁,对性能影响较大)或代码块(细粒度锁)级别。锁定方法往往不是一个很好的选择,取而代之的我们应该只锁定那些访问共享资源的代码块,因为每一个对象都有一个锁,所以可以通过创建虚拟对象来实现代码块级别的同步,方法块级别的锁比锁定整个方法更有效。
synchronized:方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)。所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作
Java虚拟机灵活的使用锁和监视器,一个监视器负责确保只有一个线程会在同一时间执行被同步的代码。每个监视器对应一个对象的引用,在线程执行代码块的第一条指令之前,他必须持有该引用对象的锁,否则他将无法执行这段代码。一旦他获得锁,该线程就可以进入这段受到保护的代码。当线程不论以何种方式退出代码块时,他都将释放关联对象的锁。对于静态方法,需要请求类级别的锁。
互斥锁:任何时候只能允许一个线程的一个读或写操作
为什么为了线程安全而锁定一个方法或者一个代码块称为“同步”而不是“锁定”或者“被锁定”
当某个方法或者代码块被声明为”synchronized”后,保存数据的内存空间(例如堆内存)将保持被同步状态。
这意味着:当一个线程获取锁并且执行到已被声明为synchronized的方法或者代码块时,该线程首先从主堆内存空间中读取该锁定对象的所有变化,以确保其在开始执行之前拥有最新的信息。在synchronized部分执行完毕,线程准备释放锁的时候,所有针对被锁定对象的修改都将为写入主堆内存中。这样其他线程在请求锁的时候就可以获取最新的信息。
同步和互斥的区别
同步:A要继续执行需要B完成某一个操作操作才能继续进行。是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
互斥:A访问了资源B就不能去访问 必须等A访问完了才行。是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
6.如何实现线程之间协作
多线程间仅仅同步是不够的,还需要线程与线程协作(通信),生产者/消费者模式是一个经典的线程同步以及通信的模型。
当使用线程来同时运行多个任务时,可以通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。
下一步就是如何使任务彼此之间可以协作通信。
同步的核心问题在于:如何保证同一资源被多个线程并发访问时的完整性,
常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。Java语言在多线程编程上实现了完全对象化,提供了对同步机制的良好支持。
Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。管道方法不建议使用,阻塞队列方法在问题4已有描述,现只提供前两种实现方法。
- wait()/notify()方法
- await()/signal()方法 消息传递机制lock
- BlockingQueue阻塞队列方法
- PipedInputStream/PipedOutputStream 管道通信
生产者-消费者模式:
生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。
wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。
notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。
notifyAll:将对象中等待池中的线程,全部放入锁池。
(notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,不过能保证程序的正确性)
工作流程:
a、Consumer线程A 来 看产品,发现产品为空,调用产品对象的wait(),线程A进入产品对象的等待池并释放产品的锁。
b、Producer线程B获得产品的锁,执行产品的notifyAll(),Consumer线程A从产品的等待池进入锁池,Producer线程B生产产品,然后退出释放锁。
c、Consumer线程A获得产品锁,进入执行,发现有产品,消费产品,然后退出。
7.守护线程
线程默认是非守护线程,非守护线程也称用户线程,一个线程调用setDaemon(boolean on)方法可以将自己设置成一个守护(Daemon)线程,如:
thread.setDaemon(true);
一个线程必须在自己运行之前设置自己是否是守护线程。守护线程是当程序中所有用户线程都已结束运行时,即使守护线程的run()方法还有需要执行的语句,守护线程也会立刻结束运行。
8.线程的常用方法
start(),run(), sleep(int millsecond), isAlive(),currentThread(), interrupt()
wait():表示等待获取某个锁, 执行了该方法的线程释放对象的锁,JVM会把该线程放到对象的等待池中。该线程等待其它线程唤醒 ,若设置参数,时间到时,线程就自动进入可执行状态.
notify(): 执行该方法的线程唤醒在对象的等待池中等待的一个线程,JVM从对象的等待池中随机选择一个线程,把它转到对象的锁池中。使线程由阻塞队列进入就绪状态
sleep():让当前正在执行的线程休眠,在休眠期间,并不释放所持有的“锁”,给低优先级的线程一个机会。必须捕获异常
yield(): 尝试让出所占有的CPU资源,让其他线程获取运行机会,不释放锁
join():则是主线程等待子线程完成,再往下执行。
9.什么是线程池
线程池作用就是限制系统中执行线程的数量。存放线程的容器就是线程池
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
比较重要的几个类:
ExecutorService |
真正的线程池接口。 |
ScheduledExecutorService |
能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 |
ThreadPoolExecutor |
ExecutorService的默认实现。 |
ScheduledThreadPoolExecutor |
继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
1. newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4. newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
线程组:
线程组存在的意义,首要原因是安全。
java默认创建的线程都是属于系统线程组,而同一个线程组的线程是可以相互修改对方的数据的。
但如果在不同的线程组中,那么就不能“跨线程组”修改数据,可以从一定程度上保证数据安全。
线程池:
线程池存在的意义,首要作用是效率。
线程的创建和结束都需要耗费一定的系统时间(特别是创建),不停创建和删除线程会浪费大量的时间。所以,在创建出一条线程并使其在执行完任务后不结束,而是使其进入休眠状态,在需要用时再唤醒,那么 就可以节省一定的时间。
如果这样的线程比较多,那么就可以使用线程池来进行管理。保证效率。
10.死锁,如何避免
死锁就是两个或两个以上的线程被无限的阻塞,线程之间相互等待所需资源。这种情况可能发生在当两个线程尝试获取其它资源的锁,而每个线程又陷入无限等待其它资源锁的释放,除非一个用户进程被终止。就JavaAPI而言,线程死锁可能发生在一下情况。
●当两个线程相互调用Thread.join()
●当两个线程使用嵌套的同步块,一个线程占用了另外一个线程必需的锁,互相等待时被阻塞就有可能出现死锁。
死锁的发生必须满足以下四个条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。
“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象 时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。由于这个原因,在使用“synchronized”关键词时,很容易出现两个线程互相等待对方做出某个动作的情形。
11. 高级同步机制(比synchronized更灵活的加锁机制)
synchronized和volatile,以及wait、notify等方法抽象层次低,在程序开发中使用比较繁琐,易出错。
而多线程之间的交互来说,存在某些固定的模式,如生产者-消费者和读者-写者模式,把这些模式抽象成高层API,使用起来会非常方便。
java.util.concurrent包为多线程提供了高层的API,满足日常开发中的常见需求。
常用接口
(1)、Lock接口,表示一个锁方法:
a、lock(),获取锁,如果无法获取所锁,会处于等待状态
b、unlock(),释放锁。(一般放在finally代码块中)
c、lockInterruptibly(),与lock()类似,但允许当前线程在等待获取锁的过程中被中断。(所以要处理InterruptedException)
d、tryLock(),以非阻塞方式获取锁,如果无法获取锁,则返回false。(tryLock()的另一个重载可以指定超时,如果指定超时,当无法获取锁,会等待而阻塞,同时线程可以被中断)
(2)、ReadWriteLock接口,表示两个锁,读取的共享锁和写入的排他锁。(适合常见的读者--写者场景)
ReadWriteLock接口的readLock和writeLock方法来获取对应的锁的Lock接口的实现。
在多数线程读取,少数线程写入的情况下,可以提高多线程的性能,提高使用该数据结构的吞吐量。
如果是相反的情况,较多的线程写入,则接口会降低性能。
(3)、ReentrantLock类和ReentrantReadWriteLock,分别为上面两个接口的实现类。
他们具有重入性:即允许一个线程多次获取同一个锁(他们会记住上次获取锁并且未释放的线程对象,和加锁的次数,getHoldCount())
同一个线程每次获取锁,加锁数+1,每次释放锁,加锁数-1,到0,则该锁被释放,可以被其他线程获取。
ReentrantLock 和synchronized 都是 可重入锁
注:重入性减少了锁在各个线程之间的等待,例如便利一个HashMap,每次next()之前加锁,之后释放,可以保证一个线程一口气完成便利,而不会每次next()之后释放锁,然后和其他线程竞争,降低了加锁的代价, 提供了程序整体的吞吐量。(即,让一个线程一口气完成任务,再把锁传递给其他线程)。
(4)、Condition接口,Lock接口代替了synchronized,Condition接口替代了object的wait、notify。
a、await(),使当前线程进入等待状态,知道被唤醒或中断。重载形式可以指定超时时间。
b、awaitNanos(),以纳秒为单位等待。
c、awaitUntil(),指定超时发生的时间点,而不是经过的时间,参数为java.util.Date。
d、awaitUninterruptibly(),前面几种会响应其他线程发出的中断请求,他会无视,直到被唤醒。
注:与Object类的wait()相同,await()会释放其所持有的锁。
e、signal()和signalAll, 相当于 notify和notifyAll
(5)阻塞队列方式
BlockingQueue接口:线程安全的阻塞式队列;当队列已满时,想队列添加会阻塞;当队列空时,取数据会阻塞。(非常适合消费者-生产者模式)
阻塞方式:put()、take()。
非阻塞方式:offer()、poll()。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
12. Java中的volatile关键字是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
volatile在多线程中是用来同步变量的。 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。
volatile就是用来避免这种情况的。volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的(也就是上面说的A) 变量。
一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。
volatile是变量修饰符,而synchronized则作用于一段代码或方法
13.Java 内存模型是什么?
Java 内存模型规定和指引 Java 程序在不同的内存架构、CPU 和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java 内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如,先行发生关系确保了:
- 线程内的代码能够按先后顺序执行,这被称为程序次序规则。
- 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
- 前一个对
volatile
的写操作在后一个volatile
的读操作之前,也叫volatile
变量规则。 - 一个线程内的任何操作必需在这个线程的 start ()调用之后,也叫作线程启动规则。
- 一个线程的所有操作都会在线程终止之前,线程终止规则。
- 一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
- 可传递性
14.多线程编程的注意事项
1、明确目的,为什么要使用多线程?如果是由于单线程读写或者网络访问(例如HTTP访问互联网)的瓶颈,可以考虑使用线程池。如果是对不同的资源(例如SOCKET连接)进行管理,可以考虑多个线程。
2、线程使用中要注意,如何控制线程的调度和阻塞,例如利用事件的触发来控制线程的调度和阻塞,也有用消息来控制的。
3、线程中如果用到公共资源,一定要考虑公共资源的线程安全性。一般用LOCK锁机制来控制线程安全性。一定要保证不要有死锁机制
4、合理使用sleep,何时Sleep,Sleep的大小要根据具体项目,做出合理安排。一般原则非阻塞状态下每个循环都要有SLeep,这样保证减少线程对CPU的抢夺。每次线程的就绪和激活都会占用一定得资源,如果线程体如果有多个循环,多处使用SLEEP将导致性能的下降。
5、线程的终止一般要使线程体在完成一件工作的情况下终止,一般不要直接使用抛出线程异常的方式终止线程。
6、线程的优先级一定根据程序的需要要有个整体的规划。