**多线程 **
1.相比于多进程,多线程的优势有:
(1)进程之间不能共享数据,线程可以;
(2)系统创建进程需要为该进程重新分配系统资源,故创建线程代价比较小;
2.创建线程和启动(3种)
(1)继承Thread类,重写run()方法(用匿名类)
Thread thread = new Thread(){
public void run(){
};
}
t.start();
(2) 实现Runnable接口,重写run方法
两种写法:
匿名:
Runnable task = new Runnable(){
public void run()
{
}
};
Thread t = new Thread( task );
t.start();
Lambda表达式
Runnable task = () -> {
System.out.println("HelloWorld");
};
Thread t = new Thread( task );
(3)通过Callable和Future创建线程
Callable的特点:
1.可以有返回值
2.接口的方法抛出Exception,如果在任务主体里面有异常,可以不处理,系统自动处理
使用Callable的步骤:
1.创建Callable的实例
Callable<String> call = () -> { return "xxx"; };
2.包装成一个FutureTask(实现了Future和Runable接口)
// FutureTask的泛型参数,必须和Callable的泛型参数一样,要求相同类型、兼容类型
FutureTask<String> task = new FutureTask<>( call );
3.把FutureTask作为任务,传递给Thread的构造器
Thread t = new Thread( task );
4.调用线程的start方法
t.start();
**3.线程的生命周期 ** (1) 、新建状态
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。
(2)、就绪状态
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一会儿,转去执行子线程。
(3)、运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
注: 当发生如下情况时,线程会从运行状态变为阻塞状态:
①、线程调用sleep方法主动放弃所占用的系统资源
②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④、线程在等待某个通知(notify)
⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
(4)、阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:
(5)、死亡状态
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
4.线程管理
(1)线程睡眠--sleep
Thread.sleep(1000);
(2)线程让步--yield
Thread.yield();
设置优先级:thread.setPriority(1);
注:关于sleep()方法和yield()方的区别如下:
①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
(3)线程合并 --join (thread.join() )
将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时
有三个重载方法:
void join() 当前线程等该加入该线程后面,等待该线程终止。
void join(long millis) 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
(4)设置线程的优先级(thread.setPriority(1) )
优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//设置线程的名称
setPriority(pro);//设置线程的优先级
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
}
}
}
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}
(5)后台(守护)进程 --thread.setDaemon(true);
把线程对象设置为后台线程,此方法必须在start()之前调用。
后台线程主要用于维护、监控任务。
所有的非后台线程结束后,表示程序要结束。此时如果还有后台线程正在执行,那么所有的后台线程直接结束、中断。
(6)正确结束线程
废弃方法 Thread.stop(); Thread.suspend(); Thread.resume();
①正常执行完run方法,然后结束掉;
②控制循环条件和判断条件的标识符来结束掉线程。
5.线程同步(同步锁)
多线程并发时,多个线程同时操作一个可共享的资源时,将会导致数据不准确。
(1)同步方法
既有synchronized关键字修饰的方法。由于java每一个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则处于阻塞状态。
注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
(2)同步代码块
既有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
注:同步是一种高开销的操作,因此应尽量减少同步的内容。
(3)使用重入锁(Lock)实现线程同步
ReentrantLock类是可重入、互斥、实现了Lock接口的锁。
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
class Bank {
private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
6.线程通信(生产者和消费者)
(1)、借助于Object类的wait()、notify()和notifyAll()实现通信
线程执行wait()后,就放弃了运行资格,处于冻结状态;
线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
notifyall(), 唤醒线程池中所有线程。
注:
① wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
② wait(),notify(),notifyall(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。
有一个字段flag来判断生产品的数量是否为空,消费品是否为空。true表示产品有,通知消费者消费;false就是没有商品
true:生产者等待消费,消费者通知,并设置为false
false:消费者等待生产,生产者通知,并设置为true
class Resource{
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
while(flag)
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
this.notifyAll();
}
public synchronized void out(){
while(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
this.notifyAll();
}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
Thread t3=new Thread(pro);
Thread t4=new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
(2)、使用Condition控制线程通信
jdk1.5中,提供了多线程的升级解决方案为:
①将同步synchronized替换为显式的Lock操作;
②将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;
③一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。
class Resource{
private String name;
private int count=1;
private boolean flag=false;
private Lock lock = new ReentrantLock();/Lock是一个接口,ReentrantLock是该接口的一个直接子类。/
private Condition condition_pro=lock.newCondition(); /创建代表生产者方面的Condition对象/
private Condition condition_con=lock.newCondition(); /使用同一个锁,创建代表消费者方面的Condition对象/
public void set(String name){
lock.lock();//锁住此语句与lock.unlock()之间的代码
try{
while(flag)
condition_pro.await(); //生产者线程在conndition_pro对象上等待
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
condition_con.signalAll();
}
finally{
lock.unlock(); //unlock()要放在finally块中。
}
}
public void out(){
lock.lock(); //锁住此语句与lock.unlock()之间的代码
try{
while(!flag)
condition_con.await(); //消费者线程在conndition_con对象上等待
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/
}
finally{
lock.unlock();
}
}
}
(3)、使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
BlockingQueue提供如下两个支持阻塞的方法:
①put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
②take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:
①在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
②在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
③在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。
public class BlockingQueueTest{ public static void main(String[] args)throws Exception{ //创建一个容量为1的BlockingQueue
BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
//启动3个生产者线程
new Producer(b).start();
new Producer(b).start();
new Producer(b).start();
//启动一个消费者线程
new Consumer(b).start();
}
} class Producer extends Thread{ private BlockingQueue<String> b;
public Producer(BlockingQueue<String> b){
this.b=b;
}
public synchronized void run(){
String [] str=new String[]{
"java",
"struts",
"Spring"
};
for(int i=0;i<9999999;i++){
System.out.println(getName()+"生产者准备生产集合元素!");
try{
b.put(str[i%3]);
sleep(1000);
//尝试放入元素,如果队列已满,则线程被阻塞
}catch(Exception e){System.out.println(e);}
System.out.println(getName()+"生产完成:"+b);
}
}
} class Consumer extends Thread{ private BlockingQueue<String> b; public Consumer(BlockingQueue<String> b){ this.b=b; } public synchronized void run(){
while(true){
System.out.println(getName()+"消费者准备消费集合元素!");
try{
sleep(1000);
//尝试取出元素,如果队列已空,则线程被阻塞
b.take();
}catch(Exception e){System.out.println(e);}
System.out.println(getName()+"消费完:"+b);
}
}
**7.线程池 **
线程池的核心:
①.创建一堆的线程放到内存里面备用。每个线程的run方法都不会结束。 在没有任务的时候,wait状态。
②如果有计算任务到达,就从线程池里面获取一个线程对象出来,并且把任务设置给线程对象。
设置完以后,发送notify通知线程要执行任务。
③任务执行完成以后,就会把线程放回线程池,并且进入wait状态。
合理利用线程池能够带来三个好处。
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
使用Executors工厂类产生线程池
Executor线程池框架的最大优点是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,然后提交即可
ExecutorService(实现类 ThreadPoolExecutor,ScheduledThreadPoolExecutor)继承了Executor接口(注意区分Executor接口和Executors工厂类),
使用Executors执行多线程任务的步骤如下:
• 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池;
• 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;
• 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;
• 当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。
【重点】ThreadPoolExecutor
简单的线程池,就是创建线程备用的。
创建3个Runnable对象,这个对象里面每次执行都需要3秒钟。
把3个任务,提交给线程池,但是线程池的大小是1,意味着最多同时执行1个任务。
ExecutorService pool = Executors.newFixedThreadPool( 大小 );
ScheduledThreadPoolExecutor
可以调度的线程池,里面的任务可以按照一定的规则循环、重复执行。
定时任务,一般会使用spring-timer来代替,支持更加复杂的任务调度方式。
ScheduledExecutorService pool = Executors.newScheduledThreadPool( 大小 );
定时重复调用的方法:
scheduleAtFixedRate : 以固定的频率执行任务,以任务的开始时间计算频率。
假设间隔2秒,每次执行任务需要3秒。
频率的间隔比任务所需要的时间要小。
此时前面的任务完成以后,马上执行下一次任务。
*间隔以开始时间计算
scheduleWithFixedDelay : 以固定的间隔执行任务
8.死锁
产生死锁的四个必要条件如下。当下边的四个条件都满足时即产生死锁,即任意一个条件不满足既不会产生死锁。
(1)死锁的四个必要条件 互斥条件:资源不能被共享,只能被同一个进程使用 请求与保持条件:已经得到资源的进程可以申请新的资源 非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源
举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。
(2)处理死锁的方法
忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;
检测死锁并恢复;
资源进行动态分配;
破除上面的四种死锁条件之一。
9.线程相关类
ThreadLocal
ThreadLocal它并不是一个线程,而是一个可以在每个线程中存储数据的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到该线程的数据。 即多个线程通过同一个ThreadLocal获取到的东西是不一样的,就算有的时候出现的结果是一样的(偶然性,两个线程里分别存了两份相同的东西),但他们获取的本质是不同的。使用这个工具类可以简化多线程编程时的并发访问,很简洁的隔离多线程程序的竞争资源。
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
若多个线程之间需要共享资源,以达到线程间的通信时,就使用同步机制;若仅仅需要隔离多线程之间的关系资源,则可以使用ThreadLocal。