Java学习之多线程

多线程相关总结:
万字图解Java多线程
Java(Android)线程池
面试官:说说多线程并发问题

一、概述:

1、线程是什么呢?
我们先来说一说比较熟悉的进程吧,之后就比较容易理解线程了。所谓进程,就是一个正在执行(进行)中的程序。每一个进程的执行都有一个执行顺序,或者说是一个控制单元。简单来说,就是你做一件事所要进行的一套流程。线程,就是进程中的一个独立的控制单元;也就是说,线程是爱控制着进程的执行。一个进程至少有一个线程,并且线程的出现使得程序要有效率。打个比方说,在仓库搬运货物,一个人搬运和五个人搬运效率是不一样的,搬运货物的整个程序,就是进程;每一个人搬运货物的过程,就是线程。

2、java中的线程:
在java中,JVM虚拟机启动时,会有一个进程为java.exe,该程序中至少有一个线程负责java程序的执行;而且该程序运行的代码存在于main方法中,该线程称之为主线程。其实,JVM启动时不止有一个线程(主线程),由于java是具有垃圾回收机制的,所以,在进程中,还有负责垃圾回收机制的线程。

3、多线程的意义:
透过上面的例子,可以看出,多线程有两方面的意义:
1)提高效率。【运行更快,CPU资源利用更充分】
2)清除垃圾,解决内存不足的问题。

二、自定义线程:

线程有如此的好处,那要如何才能通过代码自定义一个线程呢?其实,线程是通过系统创建和分配的,java是不能独立创建线程的;但是,java是可以通过调用系统,来实现对进程的创建和分配的。java作为一种面向对象的编程语言,是可以将任何事物描述为对象,从而进行操作的,进程也不例外。我们通过查阅API文档,知道java提供了对线程这类事物的描述,即Thread类。创建新执行线程有两种方法:

1、创建线程方式

方式一:继承Thread类

  • 局限性:
    单继承的局限性
    任务中的成员变量不共享,加入static才能共享

1、步骤:
第一、定义类继承Thread。
第二、复写Thread类中的run方法。
第三、调用线程的start方法。分配并启动该子类的实例。
start方法的作用:启动线程,并调用run方法。

class Demo extends Thread  
{  
    public void run()  
    {  
        for (int i=0;i<60;i++)  
            System.out.println(Thread.currentThread().getName() + "demo run---" + i);  
    }  
}  
class Test2  
{  
    public static void main(String[] args)   
    {  
        Demo d1 = new Demo();//创建一个对象就创建好了一个线程  
        Demo d2 = new Demo();  
        d1.start();//开启线程并执行run方法  
        d2.start();  
        for (int i=0;i<60;i++)  
            System.out.println("Hello World!---" + i);  
    }  
}

2、运行特点:
A.并发性:
我们看到的程序(或线程)并发执行,其实是一种假象。有一点需要明确:;在某一时刻,只有一个程序在运行(多核除外),此时cpu是在进行快速的切换,以达到看上去是同时运行的效果。由于切换时间是非常短的,所以我们可以认为是在并发进行。

B.随机性:
在运行时,每次的结果不同。由于多个线程都在获取cpu的执行权,cpu执行到哪个线程,哪个线程就会执行。可以将多线程运行的行为形象的称为互相抢夺cpu的执行权。这就是多线程的特点,随机性。执行到哪个程序并不确定。

3、覆盖run方法的原因:
1)Thread类用于描述线程。该类定义了一个功能:用于存储线程要运行的代码,该存储功能即为run方法。也就是说,Thread类中的run方法用于存储线程要运行的代码,就如同main方法存放的代码一样。

2)复写run的目的:将自定义代码存储在run方法中,让线程运行要执行的代码。直接调用run,就是对象在调用方法。调用start(),开启线程并执行该线程的run方法。如果直接调用run方法,只是将线程创建了,但未运行。

方式二:实现Runnable接口

  • 局限性:
    没有返回值
    任务无法抛异常给调用者

1、步骤:
第一、定义类实现Runnable接口。
第二、覆盖Runnable接口中的run方法。
第三、通过Thread类建立线程对象。要运行几个线程,就创建几个对象。
第四、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
第五、调用Thread类的start方法开启线程,并调用Runnable接口子类的run方法。

//多个窗口同时卖票  
class Ticket implements Runnable  
{  
    private int tic = 20;  
    public void run()  
    {  
        while(true)  
        {  
            if (tic > 0)  
                System.out.println(Thread.currentThread().getName() + "sale:" + tic--);  
        }  
    }  
}  
  
class  TicketDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t);//创建一个线程  
        Thread t2 = new Thread(t);//创建一个线程  
        Thread t3 = new Thread(t);//创建一个线程  
        Thread t4 = new Thread(t);//创建一个线程  
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }  
}

2、说明:
A.步骤2覆盖run方法:将线程要运行的代码存放在该run方法中。
B.步骤4:为何将Runnable接口的子类对象传给Thread构造函数。因为自定义的run方法所属对象为Runnable接口的子类对象,所以让线程指定对象的run方法,就必须明确该run方法所属的对象。

方式三:实现Callable接口

利用FutureTask执行任务

// 实现接口
class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        log.info("我是实现Callable的任务");
        return "success";
    }
}

// 执行
FutureTask<String> target = new FutureTask<>(new MyCallable());
new Thread(target).start();
log.info(target.get());

需要注意的是:局部变量在每一个线程中都独有一份。

2、Thread类中的一些方法简介:

在这简单介绍几个Thread中的方法:

1、线程名称

  • 获取线程名称:getName()
    每个线程都有自己默认的名称,
    也就是说,线程一为:Thread-0,线程二为:Thread-1。
    也可以获取当前线程对象的名称,通过currentThread().getName()
// 调用
对象.getName();
// 结果
Thread-编号(从0开始)
  • 设置线程名称:setName()或构造函数
    可以通过setName()设置线程名称,或者通过含有参数的构造函数直接显式初始化线程的名称。如:Test(String name)

2、线程的礼让:

  • 优先级:setPriority()
    在Thread中,存在着1~10这十个执行级别;但是并不是优先级越高,就会一直执行这个线程,只是说会优先执行到这个线程,此后还是有其他线程会和此线程抢夺cpu执行权的。
    cpu比较忙时,优先级高的线程获取更多的时间片
    cpu比较闲时,优先级设置基本没用
    优先级是可以设定的,可通过setPriority()设定
//最低
 public final static int MIN_PRIORITY = 1;
//默认
 public final static int NORM_PRIORITY = 5;
// 最高
 public final static int MAX_PRIORITY = 10;

 // 方法的定义
 public final void setPriority(int newPriority) {
 }
  • yield()
    此方法可暂停当前线程,而执行其他线程。通过这个方法,可稍微减少线程执行频率,达到线程都有机会平均被执行的效果。
    即让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。
    如下示例:t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。
// 方法的定义
public static native void yield();

Runnable r1 = () -> {
    int count = 0;
    for (;;){
       log.info("---- 1>" + count++);
    }
};
Runnable r2 = () -> {
    int count = 0;
    for (;;){
        Thread.yield();
        log.info("            ---- 2>" + count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();

// 运行结果
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield -             ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518

3、守护线程:setDaemon()

  • 默认情况下,java进程需要等待所有线程都运行结束,才会结束;
    有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。
    默认的线程都是非守护线程。
  • 垃圾回收线程就是典型的守护线程
  • 可将一个线程标记为守护线程,直接调用setDaemon()方法。
    此方法需要在启动前调用守护线程在这个线程结束后,会自动结束,则Jvm虚拟机也结束运行。
    ........  
      
    //守护线程(后台线程),在启动前调用。后台线程自动结束  
    t1.setDaemon(true);  
    t2.setDaemon(true);  
    t1.start();  
    t2.start();  
  
     .........

4、线程的阻塞:

  • 阻塞方式:
    BIO阻塞,即使用了阻塞式的IO流
    sleep(long time) 让线程休眠进入阻塞状态
    a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
    sychronizedReentrantLock 造成线程未获得锁进入阻塞状态 (同步锁章节细说)
    获得锁之后调用 wait() 方法 也会让线程进入阻塞状态 (同步锁章节细说)
    LockSupport.park() 让线程进入阻塞状态 (同步锁章节细说)
  • sleep()
    使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行
// 方法的定义 native方法
public static native void sleep(long millis) throws InterruptedException; 

try {
   // 休眠2秒
   // 该方法会抛出 InterruptedException异常 即休眠过程中可被中断,被中断后抛出异常
   Thread.sleep(2000);
 } catch (InterruptedException异常 e) {
 }
 try {
   // 使用TimeUnit的api可替代 Thread.sleep 
   TimeUnit.SECONDS.sleep(1);
 } catch (InterruptedException e) {
 }

  • join()【临时加入线程】
    特点:当A线程执行到B线程方法时,A线程就会等待,B线程都执行完,A才会执行。join可用来临时加入线程执行。
class Demo implements Runnable{  
    public void run(){  
        for(int x=0;x<90;x++){  
            System.out.println(Thread.currentThread().getName() + "----run" + x);  
        }  
    }  
}  
  
class  JoinDemo{  
    public static void main(String[] args)throws Exception{  
        Demo d = new Demo();  
        Thread t1 = new Thread(d);  
        Thread t2 = new Thread(d);  
        t1.start();       
        t2.start();  
        t1.join();//等t1执行完了,主线程才从冻结状态恢复,和t2抢执行权。t2执不执行完都无所谓。  
        int n = 0;  
        for(int x=0;x<80;x++){  
            System.out.println(Thread.currentThread().getName() + "----main" + x);  
        }  
        System.out.println("Over");  
    }  
}

5、停止线程:

  • stop【过时】
    在Java1.5之后,就不再使用stop方法停止线程了。那么该如何停止线程呢?只有一种方法,就是让run方法结束。
    开启多线程运行,运行代码通常为循环结构,只要控制住循环,就可以让run方法结束,也就可以使线程结束。
    注: 特殊情况:当线程处于冻结状态,就不会读取标记,那么线程就不会结束。如下:
class StopThread implements Runnable{  
    private boolean flag = true;  
    public synchronized void run(){  
        while (flag){  
            try{  
                wait();  
            }catch (InterruptedException e) {  
                System.out.println(Thread.currentThread().getName() + "----Exception");  
                flag = false;  
            }  
            System.out.println(Thread.currentThread().getName() + "----run");  
        }  
    }  
    public void changeFlag(){  
        flag = false;  
    }  
}  
  
class  StopThreadDemo{  
    public static void main(String[] args) {  
        StopThread st = new StopThread();  
  
        Thread t1 = new Thread(st);  
        Thread t2 = new Thread(st);  
          
        t1.start();  
        t2.start();  
  
        int n = 0;  
        while (true){  
            if (n++ == 60){  
                st.changeFlag();  
                break;  
            }  
            System.out.println("Hello World!");  
        }  
    }  
}

这时,当没有指定的方式让冻结的线程回复打破运行状态时,就需要对冻结进行清除。强制让线程回复到运行状态来,这样就可以操作标记让线程结束。

  • interrupt()
    1、此方法是为了让线程中断,但是并没有结束运行,让线程恢复到运行状态,再判断标记从而停止循环,run方法结束,线程结束。
    2、可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
    isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记
    interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)
class StopThread implements Runnable{  
    private boolean flag = true;  
    public synchronized void run(){  
        while (flag){  
            try{  
                wait();  
            }catch (InterruptedException e){  
                System.out.println(Thread.currentThread().getName() + "----Exception");  
                flag = false;  
            }  
            System.out.println(Thread.currentThread().getName() + "----run");  
        }  
    }  
}  
  
class  StopThreadDemo{  
    public static void main(String[] args){  
        StopThread st = new StopThread();  
        Thread t1 = new Thread(st);  
        Thread t2 = new Thread(st);       
        t1.start();  
        t2.start();  
        int n = 0;  
        while (true){  
            if (n++ == 60){  
                t1.interrupt();  
                t2.interrupt();  
                break;  
            }  
            System.out.println("Hello World!");  
        }  
    }  
}

三、线程的运行状态

1、系统线程的状态

  • 初始状态:创建线程对象时的状态
  • 可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
  • 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
  • 阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
  • 终止状态: 线程执行完成或抛出异常后的状态
线程的状态

需要说明的是:

  • 阻塞状态:具备运行资格,但是没有执行权,必须等到cpu的执行权,才转到运行状态。
  • 冻结状态:放弃了cpu的执行资格,cpu不会将执行权分配给这个状态下的线程,必须被唤醒后,此线程要先转换到阻塞状态,等待cpu的执行权后,才有机会被执行到。

2、Thread类定义的线程状态

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  • NEW 线程对象被创建
  • RUNNALE 线程调用了start()方法后进入该状态,该状态包含了三种情况
    • 就绪状态 :等待cpu分配时间片
    • 运行状态:进入Runnable方法执行任务
    • 阻塞状态:BIO 执行阻塞式io流时的状态
  • BLOCKED 没获取到锁时的阻塞状态(同步锁章节会细说)
  • WAITING 调用wait()、join()等方法后的状态
  • TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
  • TERMINATED 线程执行完成或抛出异常后的状态
Thread线程状态

四、上下文切换

CPU在多个线程间进行调度,运行时会进行上下文切换

发生切换场景:

  • 线程的cpu时间片用完
  • 垃圾回收
  • 线程自己调用了sleepyieldwaitjoinparksynchronizedlock 等方法

当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。

五、多线程的安全问题:

在那个简单的卖票小程序中,发现打印出了0、-1、-2等错票,也就是说这样的多线程在运行的时候是存在一定的安全问题的。

1、为什么会出现这种安全问题呢?

原因是当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还未执行完,另一线程就参与进来执行了,导致共享数据发生错误。

以也就是说,由于cpu的快速切换,当执行线程一时,tic为1了,执行到if (tic > 0)的时候,cpu就可能将执行权给了线程二,那么线程一就停在这条语句了,tic还没减1,仍为1;线程二也判断if (tic > 0)是符合的,也停在这,以此类推。

当cpu再次执行线程一的时候,打印的是1号,执行线程二的时候,是2号票,以此类推,就出现了错票的结果。其实就是多条语句被共享了,如果是一条语句,是不会出现此种情况的。

  • 问题根源:一行代码编译成字节码的时候可能为多行,在多个线程上下文切换时就可能交错执行。

2、线程安全

  • 线程安全:多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就保证了线程安全。

线程安全的类一定所有的操作都线程安全吗?
开发中经常会说到一些线程安全的类,如ConcurrentHashMap,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的。

成员变量和静态变量是否线程安全?

  • 如果没有多线程共享,则线程安全
  • 如果存在多线程共享
    • 多线程只有读操作,则线程安全
    • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 局部变量引用的对象未必是线程安全的
    • 如果该对象没有逃离该方法的作用范围,则线程安全
    • 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全

3、那么该如何解决呢?(synchronized

对于多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可参与执行,就不会出现问题了。Java对于多线程的安全问题,提供了专业的解决方式,即同步代码块,可操作共享数据。

1、同步代码块

synchronized(对象)//对象称为锁旗标  
{  
    需要被同步的代码  
}

其中的对象如同锁,持有锁的线程可在同步中执行,没有锁的线程,即使获得cpu的执行权,也进不去,因为没有获取锁,是进不去代码块中执行共享数据语句的。

同步的前提:

  • A.必须有两个或两个以上的线程
  • B.必须保证同步的线程使用同一个锁。必须保证同步中只能有一个线程在运行。

好处与弊端:

  • 解决了多线程的安全问题(阻塞式的解决方案)。
  • 多个线程需要判断锁,较为消耗资源。
class Ticket implements Runnable  
{  
    private int tic = 100;  
    Object obj = new Object();  
    public void run()  
    {  
        while(true)  
        {  
            synchronized(obj)//任意的一个对象  
            {  
                //此两句为共享语句  
                if (tic > 0)  
                    System.out.println(Thread.currentThread().getName() + "sale:" + tic--);  
            }     
        }  
    }  
}  
  
class  TicketDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t,"1");//创建第一个线程  
        Thread t2 = new Thread(t,"2");//创建第二个线程  
        //开启线程  
        t1.start();  
        t2.start();  
    }  
}

2、同步函数
同步函数就是将修饰符synchronized放在返回类型的前面,下面通过同步函数给出多线程安全问题的具体解决方案:

1)目的:判断程序中是否有安全问题,若有,该如何解决。
2)解决:
第一、明确哪些代码是多线程的运行代码
第二、明确共享数据
第三、明确多线程运行代码中,哪些语句是操作共享数据的。

示例:

class Bank  
{  
    private int sum;//共享数据  
    //run中调用了add,所以其也为多线程运行代码  
    public synchronized void add(int n)//同步函数,用synchronized修饰  
    {  
        //这有两句操作,是操作共享数据的  
        sum += n;  
            System.out.println("sum" + sum);  
    }  
}  
  
class Cus implements Runnable  
{  
    private Bank b = new Bank();//共享数据  
    //多线程运行代码run  
    public void run()  
    {  
        for (int i=0;i<3;i++)  
        {  
            b.add(100);//一句,不会分开执行,所以没问题  
        }  
    }  
}  
  
class BankDemo   
{  
    public static void main(String[] args)   
    {  
        Cus c = new Cus();  
        Thread t1 = new Thread(c);  
        Thread t2 = new Thread(c);  
        t1.start();  
        t2.start();  
    }  
}

六、同步函数中的锁:

1、非静态同步函数中的锁:this

函数需被对象调用,那么函数都有一个所属的对象引用,就是this,因此同步函数使用的锁为this。

测验如下:

class Ticket implements Runnable  
{  
    private int tic = 100;  
    boolean flog = true;  
    public void run()  
    {  
        if (flog)  
        {  
            //线程一执行  
            while(true)  
            {  
                //如果对象为obj,则是两个锁,是不安全的;换成this,为一个锁,会安全很多  
                synchronized(this)  
                {  
                    if (tic > 0)  
                        System.out.println(Thread.currentThread().getName() + "--cobe--:" + tic--);  
                }  
            }  
        }  
        //线程二执行  
        else  
            while(true)  
                show();  
    }  
    public synchronized void show()  
    {  
        if (tic > 0)  
            System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);  
    }  
}  
  
class ThisLockDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t);//创建一个线程  
        Thread t2 = new Thread(t);//创建一个线程  
        t1.start();  
        t.flog = false;//开启线程一,即关闭if,让线程二执行else中语句  
        t2.start();  
    }  
}

让线程一执行打印cobe的语句,让线程二执行打印show的语句。如果对象换位另一个对象obj,那将是两个锁,因为在主函数中创建了一个对象即Ticket t = new Ticket();,线程会共享这个对象调用的run方法中的数据,所以都是这个t对象在调用,那么,其中的对象应为this;否则就破坏了同步的前提,就会出现安全问题。

2、静态同步函数中的锁:

如果同步函数被静态修饰后,经验证,使用的锁不是this了,因为静态方法中不可定义this,所以,这个锁不再是this了。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class;该对象的类型是Class。

所以静态的同步方法使用的锁是该方法所在类的字节码文件对象,即类名.class。

示例:

>class Ticket implements Runnable  
{  
    //私有变量,共享数据  
    private static int tic = 100;  
    boolean flog = true;  
    public void run()  
    {  
        //线程一执行  
        if (flog)  
        {  
            while(true)  
            {  
                synchronized(Ticket.class)//不再是this了,是Ticket.class  
                {  
                    if (tic > 0)  
                        System.out.println(Thread.currentThread().getName() + "--obj--:" + tic--);  
                }  
            }  
        }  
        //线程二执行  
        else  
            while(true)  
                show();  
    }  
    public static synchronized void show()  
    {  
        if (tic > 0)  
            System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);  
    }  
}  
  
class StaticLockDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t);//创建第一个线程  
        Thread t2 = new Thread(t);//创建第二个线程  
        t1.start();  
        t.flog = false;  
        t2.start();  
    }  
}

在之前,也提到过关于多线程的安全问题的相关知识,就是在单例设计模式中的懒汉式中,用到了锁的机制。

七、多线程间的通信:

多线程间通信是线程之间进行交互的方式,简单说就是存储资源获取资源。比如说仓库中的货物,有进货的,有出货的。还比如生产者和消费者的例子。这些都可以作为线程通信的实例。那么如何更好地实现通信呢?

先看下面的代码:

/* 
线程间通信: 
等待唤醒机制:升级版 
生产者消费者  多个 
*/  
import java.util.concurrent.locks.*;  
  
class ProducerConsumerDemo{  
    public static void main(String[] args){  
        Resouse r = new Resouse();  
        Producer p = new Producer(r);  
        Consumer c = new Consumer(r);  
        Thread t1 = new Thread(p);  
        Thread t2 = new Thread(c);  
        Thread t3 = new Thread(p);  
        Thread t4 = new Thread(c);  
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }  
}  
  
class Resouse{  
    private String name;  
    private int count = 1;  
    private boolean flag =  false;   
    private Lock lock = new ReentrantLock();  
    private Condition condition_P = lock.newCondition();  
    private Condition condition_C = lock.newCondition();  
//要唤醒全部,否则都可能处于冻结状态,那么程序就会停止。这和死锁有区别的。  
    public void set(String name)throws InterruptedException{  
        lock.lock();  
        try{  
            while(flag)//循环判断,防止都冻结状态  
                condition_P.await();  
            this.name = name + "--" + count++;  
            System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name);  
            flag = true;  
            condition_C.signal();  
        }finally{  
            lock.unlock();//释放锁的机制一定要执行  
        }         
    }  
    public void out()throws InterruptedException{  
        lock.lock();  
        try{  
            while(!flag)//循环判断,防止都冻结状态  
                condition_C.await();  
            System.out.println(Thread.currentThread().getName() + "..消费者." + this.name);  
            flag = false;  
            condition_P.signal();//唤醒全部  
        }finally{  
            lock.unlock();  
        }  
    }  
}  
  
class Producer implements Runnable{  
    private Resouse r;  
    Producer(Resouse r){  
        this.r = r;  
    }  
    public void run(){  
        while(true){  
            try{  
                r.set("--商品--");  
            }catch (InterruptedException e){}  
        }  
    }  
}  
  
class Consumer implements Runnable{  
    private Resouse r;  
    Consumer(Resouse r){  
        this.r = r;  
    }  
    public void run(){  
        while(true){  
            try{  
                r.out();  
            }catch (InterruptedException e){}  
        }  
    }  
}

1、等待唤醒机制:

1、显式锁机制和等待唤醒机制:
在JDK 1.5中,提供了改进synchronized的升级解决方案。将同步synchronized替换为显式的Lock操作,将Object中的wait,notify,notifyAll替换成Condition对象,该对象可对Lock锁进行获取。这就实现了本方唤醒对方的操作。

在这里说明几点:
1)、对于wait,notify和notifyAll这些方法都是用在同步中,也就是等待唤醒机制,这是因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。

2)、而这些方法都定义在Object中,是因为这些方法操作同步中的线程时,都必须表示自己所操作的线程的锁,就是说,等待和唤醒的必须是同一把锁。不可对不同锁中的线程进行唤醒。所以这就使得程序是不良的,因此,通过对锁机制的改良,使得程序得到优化。

3)、等待唤醒机制中,等待的线程处于冻结状态,是被放在线程池中,线程池中的线程已经放弃了执行资格,需要被唤醒后,才有被执行的资格。

2、对于上面的程序,有两点要说明:
1)、为何定义while判断标记:
原因是让被唤醒的线程再判断一次。
避免未经判断,线程不知是否应该执行,就执行本方的上一个已经执行的语句。如果用if,消费者在等着,两个生成着一起判断完flag后,cpu切换到其中一个如t1,另一个t3在wait,当t1唤醒冻结中的一个,是t3(因为它先被冻结的,就会先被唤醒),所以t3未经判断,又生产了一个。而没消费。

2)这里使用的是signal方法,而不是signalAll方法。是因为通过Condition的两个对象,分别唤醒对方,这就体现了Lock锁机制的灵活性。可以通过Contidition对象调用Lock接口中的方法,就可以保证多线程间通信的流畅性了。

对于多线程的知识,还需要慢慢积累,毕竟线程通信可以提高程序运行的效率,这样就可以让程序得到很大的优化。期待新知识······

八、线程池

1、简述:

预先创建好一些线程,任务提交时直接执行,既可以节约创建线程的时间,又可以控制线程的数量。

2、线程池的好处

  • 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
  • 提高响应速度,任务到达时,无需创建线程即可运行
  • 提供更多更强大的功能,可扩展性高

3、线程池的主要流程

  • 流程包括:线程池创建、接收任务、执行任务、回收线程的步骤

  • 线程池的构造函数:

public ThreadPoolExecutor(int corePoolSize,  //核心线程数
                          int maximumPoolSize, //最大线程数
                          long keepAliveTime,  //救急线程的空闲时间
                          TimeUnit unit,  //救急线程的空闲时间单位
                          BlockingQueue<Runnable> workQueue,  //阻塞队列
                          ThreadFactory threadFactory,  //创建线程的工厂,主要定义线程名
                          RejectedExecutionHandler handler  //拒绝策略
) {
  //......
}
  • 流程:

1、创建线程池后,线程池的状态是RUNNABLE,该状态下才能有下面的步骤
2、提交任务时,线程池会创建线程去处理任务
3、当线程池的工作线程数达到corePoolSize时,继续提交任务会进入阻塞队列
4、当阻塞队列装满时,继续提交任务,会创建救急线程来处理
5、当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略
6、当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程

  • 注意: 不是刚创建的线程是核心线程,后面创建的线程是非核心线程;线程是没有核心非核心的概念的。
  • 拒绝策略

1、调用者抛出RejectedExecutionException (默认策略)
2、让调用者运行任务
3、丢弃此次任务
4、丢弃阻塞队列中最早的任务,加入该任务

  • 提交任务的方法
// 执行Runnable
public void execute(Runnable command) {
}

// 提交Callable
public <T> Future<T> submit(Callable<T> task) {
  if (task == null) throw new NullPointerException();
   // 内部构建FutureTask
  RunnableFuture<T> ftask = newTaskFor(task);
  execute(ftask);
  return ftask;
}

// 提交Runnable,指定返回值
public Future<?> submit(Runnable task) {
  if (task == null) throw new NullPointerException();
  // 内部构建FutureTask
  RunnableFuture<Void> ftask = newTaskFor(task, null);
  execute(ftask);
  return ftask;
} 

//  提交Runnable,指定返回值
public <T> Future<T> submit(Runnable task, T result) {
  if (task == null) throw new NullPointerException();
   // 内部构建FutureTask
  RunnableFuture<T> ftask = newTaskFor(task, result);
  execute(ftask);
  return ftask;
}

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
}

4、Execetors创建线程池

  • newFixedThreadPool(定长线程池)
    核心线程数 = 最大线程数 没有救急线程
    阻塞队列无界 可能导致oom
    可控制线程最大并发数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • newCachedThreadPool(可缓存线程池)
    核心线程数是0,最大线程数无限制 ,救急线程60秒回收
    队列采用 SynchronousQueue 实现,没有容量,即放入队列后没有线程来取就放不进去
    可能导致线程数过多,cpu负担太大
    如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • newSingleThreadExecutor(单线程化的线程池)
    核心线程数和最大线程数都是1,没有救急线程,无界队列 可以不停的接收任务
    将任务串行化 一个个执行, 使用包装类是为了屏蔽修改线程池的一些参数 比如 corePoolSize
    如果某线程抛出异常了,会重新创建一个线程继续执行
    可能造成oom
    用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • newScheduledThreadPool(定长线程池)
    任务调度的线程池。可以指定延迟时间调用,可以指定隔一段时间调用
    支持定时及周期性任务执行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

5、线程池的关闭

  • shutdown()
    会让线程池状态为shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完 相当于优雅关闭

  • shutdownNow()
    会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表

6、线程池的使用

  • 配置参数:
  • cpu密集型 : 指的是程序主要发生cpu的运算
    核心线程数 = CPU核心数+1
  • IO密集型:远程调用RPC,操作数据库等,不需要使用cpu进行大量的运算。 大多数应用的场景
    核心线程数 = 核数cpu期望利用率总时间 / cpu运算时间
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容