java中关于线程的概念

不管现实多么惨不忍睹,都要持之以恒地相信,这只是黎明前短暂的黑暗而已。不要惶恐眼前的难关迈不过去,不要担心此刻的付出没有回报,别再花时间等待天降好运。真诚做人,努力做事!你想要的,岁月都会给你。java中关于线程的概念,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

10.线程
10.1.背景知识
  • 进程
    • 官方点的理解:计算机程序在某个数据集合上的运行活动,进程是操作系统进程资源分配与调度的基本单位。即:正在运行的程序(软件)。
    • 每个进程都被操作系统分配了自己独立的内存空间与系统资源,进程之间互不干扰。
    • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程与线程调度
    • 线程依赖于进程而存在,一个进程有多个子任务,而线程就是进程中的一个子任务,一个执行单元,一个顺序控制流。线程是CPU进行资源分配与调度的进本单位。JVM是多线程的。
    • 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,线程之间共享进程的数据。这个应用程序也可以称之为多线程程序。
    • 线程调度:
      • 协同式线程调度(Cooperative Thread-Scheduling):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
        • 使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,通知系统切换到另外一个线程上去。
        • 其最大的好处是实现简单,坏处是线程执行时间不可控
      • 抢占式线程调度(Preemptive Thread-Scheduling):优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。
        • 使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。
        • 其最大的好处是,线程的执行时间是可控的
        • 设置线程的优先级: 我们在java语言中设置的线程优先级,它仅仅只能被看做是一种”建议”(对操作系统的建议),实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级),线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点
          • 获取线程优先级:public final int getPriority()
          • 设置线程优先级:public final void setPriority(int newPriority)
          • 优先级的范围:1-10,默认是5
            • MIN_PRIORITY = 1
            • NORM_PRIORITY = 5
            • MAX_PRIORITY = 10
        • 抢占式调度详解:大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。这些程序是在同时运行,实际上CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。Java语言采用的是抢占式调度。
  • 串行,并行,并发。
    • 串行:一个任务接一个任务的执行
    • 并发:同一时间段内,多个任务同时执行
    • 并行:同一时刻,多个任务同时执行
      • 并行是一种理想状态下的并发。
10.2.Thread类
  • java.lang.Thread 类,API中该类中定义了有关线程的一些方法
  • 构造方法:
    • public Thread() :分配一个新的线程对象。
    • public Thread(String name) :分配一个指定名字的新的线程对象。
    • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
    • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
  • 常用方法:
    • public String getName() :获取当前线程名称。

    • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

    • public void run() :此线程要执行的任务在此处定义代码。

    • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

    • public static Thread currentThread() :返回对当前正在执行的线程对象的引用

      • Thread.sleep():参数是毫秒
      • 等效的方法 TimeUnit.SECONDS.sleep(10):休眠10秒,单位更加具体
    • public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出

    • public static void yield():暂停当前正在执行的线程对象,并执行其他线程

    • public void interrupt():方法只是改变中断状态而已,它不会中断一个正在运行的线程

    • public final void join(): 等待该线程终止,Join这行代码在哪个线程上运行,就是哪个线程在等待,等待的是使用join方法的这个线程对象执行完。

      实际上,join方法使得等待的线程阻塞,阻塞的是join这行代码在执行的那个线程上的线程。

//1.获取线程名称
public class Demo {
    public static void main(String[] args) {
       // 获取main线程名
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());

        // 创建对象
        MyThread t1 = new MyThread();

        //Thread(String name)  利用构造函数给线程设置名字
        // 分配新的 Thread 对象。

        // 给线程设置名字
        t1.setName("吴彦祖");

        // 通过getName方法得到的默认线程名  Thread-0
        String name = t1.getName();
        MyThread t2 = new MyThread("彭于晏");

        String name1 = t2.getName();
        System.out.println(name1);
        //System.out.println(name1);
        //System.out.println(name);
        //启动线程
        t1.start();
        //t2.start();
    }
}
// 继承Thread
class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    public MyThread() {
    }
    @Override
    public void run() {
        // 在run方法中也能获取到线程名
        System.out.println(Thread.currentThread().getName() + ":hello thread");
    }
}



//2.守护线程
public class ThreadDaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建对象
        ThreadDaemon threadDaemon = new ThreadDaemon();
        threadDaemon.setName("PDD");
        // 标记成守护线程
        threadDaemon.setDaemon(true);
        // 启动线程
        threadDaemon.start();
        // 休眠3s
        Thread.sleep(3000);
        //System.out.println("123");
    }
}

class ThreadDaemon extends Thread {
    // 重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "-----" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}



//3.线程合并
public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start");
        // 创建对象
        ThreadJoin threadJoin = new ThreadJoin();
        threadJoin.setName("正方形打野");
        // 开启线程
        threadJoin.start();
        threadJoin.join();

        System.out.println("main end");
    }
}

class ThreadJoin extends Thread {
    // 重写run方法

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "------" + i);
        }
    }
}




//4.线程休眠
public class ThreadSleepDemo {
    public static void main(String[] args) {
        // 创建子类对象
        ThreadSleep t = new ThreadSleep();
        // 启动线程
        t.start();
    }
}

class ThreadSleep extends Thread {
    @Override
    public void run() {
        System.out.println("sleep before");
        try {
            // 当休眠时间到,就会自己醒来。
            Thread.sleep(2000);
            // TimeUnit.MILLISECONDS.sleep() 可以按毫秒,秒,分钟,小时,天
            // TimeUnit.SECONDS.sleep(2);
            // TimeUnit.HOURS.sleep(1);
            // TimeUnit.DAYS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sleep after");
    }
}




//5.线程终止
public class ThreadStop {
    public static void main(String[] args) throws InterruptedException {
        // 创建对象
        ThreadInterrupt threadInterrupt = new ThreadInterrupt();
        // 启动线程
        threadInterrupt.start();
        Thread.sleep(3000);
        // 泼了一盆冷水,叫醒
        // 实际上是根据java当中的异常处理机制
        //threadInterrupt.interrupt();
        threadInterrupt.stop();

    }
}

class ThreadInterrupt extends Thread {
    // 重写run方法

    @Override
    public void run() {
        System.out.println("run start");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // stop方法具有固有的不安全性
        // 这里就不会再执行了
        System.out.println("run end");
    }
}



//6.线程礼让
public class ThreadYieldDemo {
    public static void main(String[] args) {
        // 创建对象
        ThreadYield threadYield = new ThreadYield();
        ThreadYield threadYield2 = new ThreadYield();

        threadYield.setName("55开");
        threadYield2.setName("卢本伟");

        // 启动2个线程
        threadYield.start();
        threadYield2.start();

    }
}

class ThreadYield extends Thread {
    // 重写run方法

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+"------"+i);
            yield();
            // 暂停当前正在执行的线程对象,
            // 并执行其他线程。
            // 虽然放弃了CPU的执行权,但是我们java当中采用的是抢占式的调度方式
            // 所以下一次不一定是谁抢到
        }
    }
}
10.3.实现多线程
  • 创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式
10.3.1.方法一
  • Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象
    3. 调用线程对象的start()方法来启动该线程
public class MyThread extends Thread{
    /*
    * 利用继承中的特点
    * 将线程名称传递 进行设置
    */
    public MyThread(String name){
    	super(name);
    }
    /*
    * 重写run方法
    * 定义线程要执行的代码
    */
    public void run(){
        for (int i = 0; i < 20; i++) {
            //getName()方法 来自父亲
            System.out.println(getName()+i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
    	System.out.println("这里是main线程");
    	MyThread mt = new MyThread("小强");
    	mt.start();//开启了一个新的线程
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财:"+i);
        }
    }
}
//对于这种方式实现的多线程,如果存在变量不一致的问题,可以采用static关键字来解决。
  • 注意事项:
    1. 一个Thread子类对象代表一个线程
    2. 只有Thread run()方法中的代码,才会执行在子线程中,保证运行其中的是我们想要在子线程中运行的代码(run方法封装的是被线程执行的任务)
    3. 一个方法,被哪个线程中的代码调用,被调用的方法,就运行在,调用它的线程中
    4. 启动线程,必须使用start()方法来启动,才能是Thread中的run方法运行在子线程中。调用run方法执行Thread的run方法代码,这仅仅只是普通的方法调用
    5. 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次,如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象
10.3.2.方式二
  • 实现 java.lang.Runnable 也是非常常见的一种,只需要重写run方法即可。
    1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    3. 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        //创建自定义类对象 线程任务对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t = new Thread(mr, "小强");
        t.start();
        for (int i = 0; i < 20; i++) {
        	System.out.println("旺财 " + i);
        }
    }
}
  1. Thread类实际上也是实现了Runnable接口的类。
  2. 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
  3. 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。
10.3.4.匿名内部类创建线程
public class NoNameInnerClassThread {
    public static void main(String[] args) {
    //new Runnable(){
    //    public void run(){
    //        for (int i = 0; i < 20; i++) {
    //            System.out.println("张宇:"+i);
    //        }
    //    }
    //}; //‐‐‐这个整体 相当于new MyRunnable()
    
        Runnable r = new Runnable(){
            public void run(){
                for (int i = 0; i < 20; i++) {
                    System.out.println("张宇:"+i);
                }
            }
        };
        new Thread(r).start();	
        for (int i = 0; i < 20; i++) {
            System.out.println("费玉清:"+i);
        }
    }
}
10.4.Thread和Runnable的区别
  • 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
  • 实现Runnable接口比继承Thread类所具有的优势:
    1. 适合多个相同的程序代码的线程去共享同一个资源。
    2. 可以避免Java中的单继承的局限性。
    3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
    4. 线程池只能放入实现Runable或Callable的类的线程,不能直接放入继承Thread的类。

    注:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程

10.5.线程安全
10.5.1.线程同步
  • 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
  • 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了线程同步机制(synchronized)来解决。
  • 三种方法:
    1. 同步代码块。
    2. 同步方法。
    3. 锁机制。
10.5.2.同步代码块
  • synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){
	//需要同步操作的代码
}

public class Ticket implements Runnable{
    private int ticket = 100;
    Object lock = new Object();
    /*
    * 执行卖票操作
    */
    @Override
    public void run() {
        //每个窗口卖票的操作
        //窗口 永远开启
        while(true){
        	synchronized (lock) {
                if(ticket>0){//有票 可以卖
                    //出票操作
                    //使用sleep模拟一下出票时间
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // TODO Auto‐generated catch block
                        e.printStackTrace();
        			}
                    //获取当前线程对象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name+"正在卖:"+ticket‐‐);
                }
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建三个窗口对象
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        //同时卖票
        t1.start();
        t2.start();
        t3.start();
    }
}

同步锁:

  1. 锁对象可以是任意的Java对象。
  2. 对于非static方法,同步锁对象就是this。
  3. 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
10.5.3.同步方法
  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){
	//可能会产生线程安全问题的代码
}

public class Ticket implements Runnable{
    private int ticket = 100;
    /*
    * 执行卖票操作
    */
    @Override
    public void run() {
        //每个窗口卖票的操作
        //窗口 永远开启
        while(true){
        	sellTicket();
        }
    }
    
    /*
    * 锁对象:是谁调用这个方法,就是谁
    * 隐含锁对象就是 this
    */
    public synchronized void sellTicket(){
        if(ticket>0){//有票 可以卖
            //出票操作
            //使用sleep模拟一下出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto‐generated catch block
                e.printStackTrace();
            }
            //获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
        }
    }
}
10.5.4.Lock锁
  • java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
  • Lock锁也称同步锁,加锁与释放锁方法化了:
    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁。
public class Ticket implements Runnable{
    private int ticket = 100;
    Lock lock = new ReentrantLock();
    /*
    * 执行卖票操作
    */
    @Override
    public void run() {
        //每个窗口卖票的操作
        //窗口 永远开启
        while(true){
        	lock.lock();
            if(ticket>0){//有票 可以卖
                //出票操作
                //使用sleep模拟一下出票时间
                try {
                	Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }
                //获取当前线程对象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在卖:"+ticket--);
            }
            lock.unlock();
        }
    }
}
10.6.线程状态
  • java.lang.Thread.State 这个枚举中给出了六种线程状态
线程状态 导致状态发生条件
New(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
10.6.1.Timed Waiting计时等待
  • 一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态
  • 当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)。
public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
                System.out.print(" 线程睡眠1秒!\n");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
    new MyThread().start();
    }
}
  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
  4. sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
10.6.2.BLOCKED(锁阻塞)
  • 一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
10.6.3.Waiting(无限等待)
  • 一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
public class WaitingTest {
    public static Object obj = new Object();
    
    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println( Thread.currentThread().getName() +"=== 象,调用wait方法,进入waiting状态,释放锁对象");
                            obj.wait(); //无限等待
                            //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了");
                    }
                }
            }
        },"等待线程").start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                // while (true){ //每隔3秒 唤醒一次
                try {
                    System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj){
                    System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象");
                    obj.notify();
                }    
            }
        // }
        },"唤醒线程").start();
    }
}
  • 通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。
  • 其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
  • 当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

注:

​ 我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

10.7.死锁
  • 死锁:线程一占用A资源,等待B资源,线程二占用B资源,等待A资源,这种互锁现象称为死锁

    个人理解:死锁如果用通俗易懂的话来解释,其实就像我们生活中,和迎面而来的人相遇,给对面的人让路,对面的人也给我们让路,然而两个人让路的方向是同一边,这样就是死锁。

  • 解决方法:
    1. 更改加锁顺序
    2. 新加一把锁,把这个操作变成原子操作
public class Demo {

    public static void main(String[] args) {
        DieLock dieLock1 = new DieLock(true);
        DieLock dieLock2 = new DieLock(false);

        new Thread(dieLock1).start();
        new Thread(dieLock2).start();

    }
}

// 描述锁对象
class MyLock {
    public static final Object objA = new Object();
    public static final Object objB = new Object();
    public static final Object objAB = new Object();
}

class DieLock implements Runnable {
    boolean flag;

    public DieLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {

                synchronized (MyLock.objA) {
                    System.out.println("if A");
                    // 发生了线程切换
                    synchronized (MyLock.objB) {
                        System.out.println("if B");
                    }
                }
            }
        }
        else {
            synchronized (MyLock.objB) {
                System.out.println("else B");
                // 打印了else B
                synchronized (MyLock.objA) {
                    System.out.println("else A");
                }
            }
        }
    
    	//解决方法:
        //1.新加一把锁
        if (flag) {
			synchronized (MyLock.objAB) {
                synchronized (MyLock.objA) {
                System.out.println("if A");
                // 发生了线程切换
                synchronized (MyLock.objB) {
                    System.out.println("if B");
                }
            }
        }else {
            synchronized (MyLock.objAB) {
                synchronized (MyLock.objB) {
                    System.out.println("else B");
                    // 打印了else B
                    synchronized (MyLock.objA) {
                        System.out.println("else A");
                    }
                }
            }
        }
        
        //2.更改加锁顺序,就是把原来程序的其中一个分支调换顺序  
    }
}
10.8.线程通信
  • 以生产者,消费者,包子铺为例子,说明线程通信
public class Box {
    // 包子
    Food food;

    // 生产包子  只有生产者线程调用setFood方法
    public synchronized void setFood(Food newFood) throws InterruptedException {
        //先判断蒸笼是否为空
        //如果蒸笼为空,没有包子
        if (food == null) {
            //做包子并放入蒸笼,
            food = newFood;
            System.out.println(Thread.currentThread().getName() + "做了:" + food);
            Thread.sleep(3000);
            // 通知消费者来吃
            notify();
            //notifyAll();
        } else {
            //如果蒸笼非空
            //说明蒸笼里有包子,
            // 阻止自己,不在生产
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //System.out.println("生产者 wait 后面的代码");
        }
    }

    // 吃包子 只有消费者线程调用eatFood方法
    public synchronized void eatFood() throws InterruptedException {
        // 先判断蒸笼状态
        if (food == null) {
            //如果蒸笼为空,没有包子
            //阻止自己
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //System.out.println("消费者 wait 后面的代码");
        } else {
            //如果蒸笼非空 有包子
            //吃包子
            System.out.println(Thread.currentThread().getName() + "吃了:" + food);
            Thread.sleep(3000);
            food = null;
            //通知生产者去再生产包子
            notify();
            //notifyAll();
        }
    }
}


// 该类描述包子
class Food {
    String name;
    int price;

    public Food(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Food{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}



public class ConsumerTask implements Runnable {
    // 蒸笼
    Box box;

    public ConsumerTask(Box box) {
        this.box = box;
    }

    @Override
    public void run() {
        while (true) {
            try {
                box.eatFood();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}



public class ProducerTask implements Runnable {
    Box box;
    Food[] foods = {new Food("庆丰包子", 8),
            new Food("上海生煎包", 2), new Food("广式叉烧包", 10)};
    Random random;

    public ProducerTask(Box box) {
        this.box = box;
        random = new Random();
    }

    @Override
    public void run() {
        while (true) {
            // 只生产包子
            int index = random.nextInt(foods.length);
            try {
                box.setFood(foods[index]);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Box box = new Box();
        ProducerTask producerTask = new ProducerTask(box);
        ConsumerTask consumerTask = new ConsumerTask(box);

        //创建2个线程并启动
        // 生产者线程
        Thread t1 = new Thread(producerTask);
        // 消费者线程
        Thread t2 = new Thread(consumerTask);

        Thread t3 = new Thread(producerTask);
        // 消费者线程
        Thread t4 = new Thread(consumerTask);

        t1.setName("生产者1");
        t2.setName("消费者1");
        t3.setName("生产者2");
        t4.setName("消费者2");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
  • 解释:
    1. 对于notify方法,两个生产者,两个消费者,当生产者生产完毕后,通知的却不一定是消费者,notify方法是随机的,如果另一个生产者抢到了执行权,进入if判断语句,发现已经有包子了,那么程序就会执行else语句,进入等待。
    2. 对于notifyAll方法,当生产者生产完毕后,唤醒所有线程,三个线程需要争夺执行权限,此时如果另一个生产者抢到了执行权,会进入阻塞状态,剩余两个消费者再次争夺,当某个消费者消费完成后,执行notifyAll方法,唤醒其余线程,进入阻塞状态的生产者会立即执行完wait后面的代码,并立即加入本轮争夺,也就是说,此次消费者消费完成后,依旧是有两个生产者争夺执行权。
    3. 某个线程想要执行,需要两个条件同时满足,第一,获取到锁对象,第二,抢到了cpu执行权,即其他线程调用了notify方法或notifyAll方法,本线程争夺到了cpu执行权。但是需要注意的是,两个条件必须同时满足,假设这样一种情形,某个程序只有两个线程,一个线程正在运行时,调用了notify方法或notifyAll方法,但是自身却没有执行wait方法释放锁对象,那么另一个线程就会进入阻塞状态,直到正在运行的线程释放了锁对象。
10.9.线程池
10.9.1.概念
  • 一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
  • 优点:
    1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
10.9.1.线程池的创建
  • Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象
    • 不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
      Executors.newCachedThreadPool();    //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
      Executors.newSingleThreadExecutor();   //创建容量为1的缓冲池
      Executors.newFixedThreadPool(int n);  //创建固定容量大小为n的缓冲池
      
    • 一般需要根据任务的类型来配置线程池大小:
      1. 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 N(CPU) + 1
      2. 如果是IO密集型任务,参考值可以设置为2*N(CPU)
  • 线程池的创建:JDK5提供了Executors来产生线程池,有如下方法
    • public ExecutorService newCachedThreadPool()
      1. 会根据需要创建新线程,也可以自动删除,60s处于空闲状态的线程
      2. 线程数量可变,立马执行提交的异步任务(异步任务:在子线程中执行的任务)
    • public ExecutorService newFixedThreadPool(int nThreads)
      1. 线程数量固定
      2. 维护一个无界队列(暂存已提交的来不及执行的任务)
      3. 按照任务的提交顺序,将任务执行完毕
    • public ExecutorService newSingleThreadExecutor()
      1. 单个线程
      2. 维护了一个无界队列(暂存已提交的来不及执行的任务)
      3. 按照任务的提交顺序,将任务执行完毕
10.9.2.线程池的使用
  • 定义了一个使用线程池对象的方法如下:
    • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  • 使用线程池中线程对象的步骤:
    1. 创建线程池对象。
    2. 创建Runnable接口子类对象。(task)
    3. 提交Runnable接口子类对象。(take task)
    4. 关闭线程池(一般不做)。
  • ExecutorService(接口):Future submit(Callable task)
    • 代表有返回值的异步任务
    • Future对象代表异步任务的计算结果 可通过get方法获取
    • public Future<?> submit(Runnable task):代表没有返回值的异步任务
  • 关闭线程池:
    • public shutdown():启动一次顺序关闭,执行以前提交的任务,但不接收新任务
    • public shutdownNow():立刻关闭
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();
        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ‐‐‐> 调用MyRunnable中的run()
        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        //线程池只有两个线程对象,此任务会进入队列,等待下一轮执行
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池,执行完全部任务后关闭
        //service.shutdown();
        //执行两个任务就会关闭
        //service.shutdownNow();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个教练");
        try {
            //这里的sleep函数可以清楚地看到执行顺序
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,教完后,教练回到了游泳池");
    }
}
10.10.定时器
  • 定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式来执行。
    • 定时器用来管理时间
    • Timer触发执行的任务就是定时任务,即TimerTask
    • 用来完成调度定时任务的功能
//创建定时器
Timer timer = new Timer()

//调度定时任务
//在指定的时间点,调度定时任务执行
schedule(TimerTask task, Date time)
//在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务
schedule(TimerTask task, long delay, long period)
//在firstTime时间点,首次执行,之后每period毫秒执行一次
schedule(TimerTask task, Date firstTime, long period)
//在delay毫秒的延时后,首次调度task执行,之后每period毫秒执行一次
scheduleAtFixedRate(TimerTask task, long delay, long period)
    
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");
  • TimerTask(抽象类):表示待执行的定时任务
  • 定义一个定时任务
    • 继承TimerTask
    • 重写run方法
  • 终止定时器
    • timer 当中的的cancle方法:定时器中所有任务都会被终止
public class Demo {
    public static void main(String[] args) throws ParseException {
        // /在指定的时间点,调度定时任务执行
        //schedule(TimerTask task, Date time)
        在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务
        //schedule(TimerTask task, long delay, long period)

        // 创建定时器
        Timer timer = new Timer();
        // 进行任务调度
        String s = "2021-01-23 11:43:40";
        // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = simpleDateFormat.parse(s);
        //timer.schedule(new MyTask(),date);

        timer.schedule(new MyTask(),3000,5000);
        timer.cancel();
    }
}

/*
**如何定义一个定时任务**
- 继承TimerTask
- 重写run方法
* */
class MyTask extends TimerTask {

    @Override
    public void run() {
        // 定时任务要做的事情写到run方法里
        System.out.println("boom! 爆炸了!");
    }
}
10.11.Callable
public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);
        // 提交任务
        Future future = pool.submit(new MyCallable());
        //get() 获取计算结果
        //如有必要,等待计算完成,然后获取其结果。
        System.out.println("get before");
        String s = (String) future.get();
        System.out.println(s);
    }
}

/*可以理解为多线程的实现方式3 ,但是必须要依赖线程池*/
class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "------" +i);
        }
        Thread.sleep(3000);
        return "hello";
    }
}
  • Runnable和Callable的区别:
    1. Callable规定的被重写的方法是call(),Runnable规定的被重写的方法是run()
    2. Callable的任务执行后可以有返回值,而Runnable的任务没有返回值
    3. call方法可以抛出异常,run方法不可以
    4. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
10.12.volatile
  • volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
  • 特性:
    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
    • 禁止进行指令重排序。(实现有序性)
    • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

      关于volatile原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

  • volatile写:当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。
  • volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
  • volatile指令重排:volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:
    第一个操作/第二个操作 普通读写 volatile读 volatile写
    普通读写 不允许
    volatile读 不允许 不允许 不允许
    volatile写 不允许 不允许

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/181089.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!