• Java学习之多线程


    进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
    线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
     在java中要想实现多线程,有两种手段,一种是继承Thread类,另外一种是实现Runable接口。Thread类实际上也是实现了Runnable接口的类。
    实现Runnable接口比继承Thread类所具有的优势:
    1):适合多个相同的程序代码的线程去处理同一个资源
    2):可以避免java中的单继承的限制
    3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
    在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾回收线程。
    线程状态转换
    1、新建状态(New):新创建了一个线程对象。
    2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
    3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。



    线程的调度
    1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
     Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
    static int MAX_PRIORITY 线程可以具有的最高优先级,取值为10。
    static int MIN_PRIORITY  线程可以具有的最低优先级,取值为1。
    static int NORM_PRIORITY  分配给线程的默认优先级,取值为5。
     
    Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
     
    每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
    线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
    JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
     
    2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
     
    3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
     
    4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
     
    5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
     
    6、 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选 择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程 在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

    线程常用方法

    join():join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是子线程调用了join()方法,之后的代码只有等到子线程结束了才能执行。在很多情况下,主线程生成并启动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
     
    sleep():使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
    sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并没有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
     
    wait():是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;

    yield():暂停当前正在执行的线程对象,并执行其他线程。Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的 线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

    sleep()和yield()的比较
    1,sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执 行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
     2,sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权, 但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的 有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级或更高优先级的其他线程
     3,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的 线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只 能等待所有较高优先级的线程运行结束,才有机会运行。 

    interrupt():中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭

    wait():Obj.wait(),与Obj.notify()必须要与 synchronized(Obj)一起使用,也就是wait与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是 Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说线程在获取对象锁 后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就 是对对象锁的唤醒。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执 行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一个线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间步、唤醒的操 作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在 释放CPU同时,释放了对象锁的控制。

     wait()和sleep()的比较
    共同点: 
    1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。 
    2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。 如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 
     需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException。wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。  不同点: 
    1. Thread类的方法:sleep(),yield()等 
       Object的方法:wait()和notify()等 
    2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 
    3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 
    4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常


    线程同步
    1、synchronized关键字的作用域有二种: 
    1) 是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线 程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法; 
    2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。 

    2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象; 

    3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
    A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
    B.每个对象只有一个锁(lock)与之相关联。
    C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

    假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。
     
    1.  把synchronized当作方法修饰符时,示例代码如下:
    Public synchronized void methodAAA()
    {
    //….
    }
    这 也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个 同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized 关键字的方法。
    上边的示例代码等同于如下代码:
    public void methodAAA()
    {
    synchronized (this)      //  (1)
    {
           //…..
    }
    }
     (1) 处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同 步机制的控制,造成数据混乱
    2.同步块,示例代码如下:
                public void method3(SomeObject so)
                 {
                         synchronized(so)
                        {                        //…..                     }               } 这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
    class Foo implements Runnable
    {
           private byte[] lock = new byte[0];  // 特殊的instance变量
        Public void methodA()
    {
           synchronized(lock) { //… }
    }
    //…..
    }
    注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
    3.将synchronized作用于static 函数,示例代码如下:
          Class Foo
    {
    public synchronized static void methodAAA()   // 同步的static 函数
    {
    //….
    }
    public void methodBBB()
    {
           synchronized(Foo.class)   //  class literal(类名称字面常量)
    }
           }
       代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
    记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。
    可 以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这 个对象,而B的锁是Obj所属的那个Class。
    线程数据传递
    在 传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式 下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和 return语句来返回数据。
    9.1、通过构造方法传递数据 
    在创建线程时,必须要建立一个Thread类的或其子类的实 例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在 run方法中使用)。下面的代码演示了如何通过构造方法来传递数据: 
     

    public class MyThread1 extends Thread 
    { 
    private String name; 
    public MyThread1(String name) 
    { 
    this.name = name; 
    } 
    public void run() 
    { 
    System.out.println("hello " + name); 
    } 
    public static void main(String[] args) 
    { 
    Thread thread = new MyThread1("world"); 
    thread.start(); 
    } 
    } 



    由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现 象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于 Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情 况,就得通过类方法或类变量来传递数据。 
    9.2、通过变量和方法传递数据 
    向对象中传入数据一般有两次机会,第一次机会是 在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实 例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量: 
     

    public class MyThread2 implements Runnable 
    { 
    private String name; 
    public void setName(String name) 
    { 
    this.name = name; 
    } 
    public void run() 
    { 
    System.out.println("hello " + name); 
    } 
    public static void main(String[] args) 
    { 
    MyThread2 myThread = new MyThread2(); 
    myThread.setName("world"); 
    Thread thread = new Thread(myThread); 
    thread.start(); 
    } 
    } 



    9.3、通过回调函数传递数据 
    上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动 将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生 了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回 value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。 
     

    class Data 
    { 
    public int value = 0; 
    } 
    class Work 
    { 
    public void process(Data data, Integer numbers) 
    { 
    for (int n : numbers) 
    { 
    data.value += n; 
    } 
    } 
    } 
    public class MyThread3 extends Thread 
    { 
    private Work work; 
    public MyThread3(Work work) 
    { 
    this.work = work; 
    } 
    public void run() 
    { 
    java.util.Random random = new java.util.Random(); 
    Data data = new Data(); 
    int n1 = random.nextInt(1000); 
    int n2 = random.nextInt(2000); 
    int n3 = random.nextInt(3000); 
    work.process(data, n1, n2, n3); // 使用回调函数 
    System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 
    + String.valueOf(n3) + "=" + data.value); 
    } 
    public static void main(String[] args) 
    { 
    Thread thread = new MyThread3(new Work()); 
    thread.start(); 
    } 
    
    应用案例
    1,车站多窗口卖票
    public class SaleTicket implements Runnable {
        private int    tickets    = 100;
        public void run() {
            while (tickets > 0) {
                synchronized (this) {
                    if (tickets > 0) {
                        System.out.println(Thread.currentThread().getName() + "卖出 第 " + (tickets--) + "张票");
                    }
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args) {
            SaleTicket st = new SaleTicket();
            Thread t1 = new Thread(st, "一号窗口");
            Thread t2 = new Thread(st, "二号窗口");
            Thread t3 = new Thread(st, "三号窗口");
            Thread t4 = new Thread(st, "四号窗口 ");
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }
    2,交替打印
    建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
    public class MyThread implements Runnable{
        private String name;
        private Object prev;
        private Object self;
        public MyThread(String name,Object prev,Object self){
            this.name = name;
            this.prev = prev;
            this.self = self;
        }
        @Override
        public void run() {
            int count = 10;
            while (count>0) {
                synchronized (prev) {
                    synchronized (self) {
                        System.out.println(name);
                        count--;
                        self.notify();
                    }
                    try {
                        prev.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        public static void main(String[] args) throws Exception{
            Object a = new Object();
            Object b = new Object();
            Object c = new Object();
            MyThread aThread = new MyThread("A", c, a);
            MyThread bThread = new MyThread("B", a, b);
            MyThread cThread = new MyThread("C", b, c);
            new Thread(aThread).start();
            Thread.sleep(100);
            new Thread(bThread).start();
            Thread.sleep(100);
            new Thread(cThread).start();
            Thread.sleep(100);
        }
    }先来解释一下其整体思路,从大的方 向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循 环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是 prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放 自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用 prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运 行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等 待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照 A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。
  • 相关阅读:
    【回顾一下Docker的基本用法】
    修复Apache Shiro身份认证绕过漏洞 (CVE-2022-32532)步骤注意事项
    vivado简单仿真入门
    Python语言学习:Python语言学习之python包/库package的简介、管理外部package的Python工具案例应用之详细攻略
    Centos 优化ulimit
    将 WSL 安装到C盘以外的位置
    以学工管理系统打造一体化智慧校园平台
    C++中嵌入汇编语言的方法(这个方法被证明在64位电脑上使用visual studio没有用)
    视频汇聚平台EasyCVR从一分屏切换到四分屏后加载记录显示黑屏该如何解决?
    设计模式之工厂模式(C++)
  • 原文地址:https://blog.csdn.net/weixin_71792169/article/details/126617694