美文网首页
Java 多线程

Java 多线程

作者: 高丕基 | 来源:发表于2018-05-23 21:34 被阅读134次

    1、概述

    Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

    2、线程状态

    线程状态图

    一个线程在创建到最后消亡中间可能处于许多不同的状态,一共大致可分为如下几个状态:

    ① 新建状态(New):新创建了一个线程对象。

    ② 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

    ③ 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

    ④ 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

    同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

    其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意sleep是不会释放持有的锁)

    ⑤ 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    3、创建线程3种方式

    3.1 通过继承 Thread 类本身

    创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。

    
    class ThreadDemo extends Thread {
    
      private Thread t;
    
      private String threadName;
    
      ThreadDemo( String name) {
    
          threadName = name;
    
          System.out.println("Creating " +  threadName );
    
      }
    
      public void run() {
    
          System.out.println("Running " +  threadName );
    
          try {
    
            for(int i = 4; i > 0; i--) {
    
                System.out.println("Thread: " + threadName + ", " + i);
    
                // 让线程睡眠一会
    
                Thread.sleep(50);
    
            }
    
          }catch (InterruptedException e) {
    
            System.out.println("Thread " +  threadName + " interrupted.");
    
          }
    
          System.out.println("Thread " +  threadName + " exiting.");
    
      }
    
      public void start () {
    
          System.out.println("Starting " +  threadName );
    
          if (t == null) {
    
            t = new Thread (this, threadName);
    
            t.start ();
    
          }
    
      }
    
    }
    
    public class TestThread {
    
      public static void main(String args[]) {
    
          ThreadDemo T1 = new ThreadDemo( "Thread-1");
    
          T1.start();
    
          ThreadDemo T2 = new ThreadDemo( "Thread-2");
    
          T2.start();
    
      } 
    
    }
    
    

    3.2 实现 Runnable 接口

    为了实现 Runnable,一个类只需要执行一个方法调用 run(), run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。Thread 定义了几个构造方法,下面的这个是我们经常使用的:

    
    Thread(Runnable threadOb,String threadName);
    
    

    这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。新线程创建之后,你调用它的 start() 方法它才会运行,例子如下:

    
    class RunnableDemo implements Runnable {
    
      private Thread t;
    
      private String threadName;
    
      RunnableDemo( String name) {
    
          threadName = name;
    
          System.out.println("Creating " +  threadName );
    
      }
    
      public void run() {
    
          System.out.println("Running " +  threadName );
    
          try {
    
            for(int i = 4; i > 0; i--) {
    
                System.out.println("Thread: " + threadName + ", " + i);
    
                // 让线程睡眠一会
    
                Thread.sleep(50);
    
            }
    
          }catch (InterruptedException e) {
    
            System.out.println("Thread " +  threadName + " interrupted.");
    
          }
    
          System.out.println("Thread " +  threadName + " exiting.");
    
      }
    
      public void start () {
    
          System.out.println("Starting " +  threadName );
    
          if (t == null) {
    
            t = new Thread (this, threadName);
    
            t.start ();
    
          }
    
      }
    
    }
    
    public class TestThread {
    
      public static void main(String args[]) {
    
          RunnableDemo R1 = new RunnableDemo( "Thread-1");
    
          R1.start();
    
          RunnableDemo R2 = new RunnableDemo( "Thread-2");
    
          R2.start();
    
      } 
    
    }
    
    

    3.3 通过 Callable 和 Future 创建线程

    创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。若此时线程还没运行结束,则调用get() 方法的线程会阻塞,知道得到返回值。

    
    public class CallableThreadTest implements Callable<Integer>{ 
    
         public static void main(String[] args)
    
         { 
    
         CallableThreadTest ctt = new CallableThreadTest(); FutureTask ft = new FutureTask<>(ctt);
    
            for(int i = 0;i < 100;i++) 
    
            { 
    
                System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i); 
    
                if(i==20) 
    
                { 
    
                    new Thread(ft,"有返回值的线程").start(); 
    
                } 
    
            } 
    
            try 
    
            { 
    
                System.out.println("子线程的返回值:"+ft.get()); 
    
            } catch (InterruptedException e) 
    
            { 
    
                e.printStackTrace(); 
    
            } catch (ExecutionException e) 
    
            { 
    
                e.printStackTrace(); 
    
            } 
    
        }
    
        @Override 
    
        public Integer call() throws Exception 
    
        { 
    
            int i = 0; 
    
            for(;i<100;i++) 
    
            { 
    
                System.out.println(Thread.currentThread().getName()+" "+i); 
    
            } 
    
            return i; 
    
        } 
    
    }
    
    

    4、Thread相关方法

    4.1 public void start()

    使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

    4.2 public void run()

    如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。一般不通过调用该方法来运行线程,而是通过start()方法。

    4.3 public final void setPriority(int priority)

    更改线程的优先级。 java 中的线程优先级的范围是1~10,默认的优先级是5。“高优先级线程”会优先于“低优先级线程”执行。线程的优先级只能确保CPU尽量将执行的资源让给优先级高的线程用,但不保证定义的高优先级的线程的大部分都能先于低优先级的线程执行完。线程的优先级具有随机性,也就是高优先级的线程不一定每一次都先执行完。

    4.4 public final void setDaemon(boolean on)

    将该线程标记为守护线程或用户线程。Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)。用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆。当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。

    4.5 public final void join(long millisec)/public final void join()

    等待该线程终止的时间最长为 millis 毫秒。如果不传参数就代表一直等待直到调用join线程运行完毕。才继续向下运行。例子如下:

    
    class Thread1 extends Thread{
    
        private String name; 
    
        public Thread1(String name) { 
    
            super(name); 
    
          this.name=name; 
    
        } 
    
        public void run() { 
    
            System.out.println(Thread.currentThread().getName() + " 线程运行开始!"); 
    
            for (int i = 0; i < 5; i++) { 
    
                System.out.println("子线程"+name + "运行 : " + i); 
    
                try { 
    
                    sleep((int) Math.random() * 10); 
    
                } catch (InterruptedException e) { 
    
                    e.printStackTrace(); 
    
                } 
    
            } 
    
            System.out.println(Thread.currentThread().getName() + " 线程运行结束!"); 
    
        } 
    
    public static void main(String[] args) {
    
            System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); 
    
            Thread1 mTh1=new Thread1("A"); 
    
            Thread1 mTh2=new Thread1("B"); 
    
            mTh1.start(); 
    
            mTh2.start(); 
    
            try { 
    
                mTh1.join(); 
    
            } catch (InterruptedException e) { 
    
                e.printStackTrace(); 
    
            } 
    
            try { 
    
                mTh2.join(); 
    
            } catch (InterruptedException e) { 
    
                e.printStackTrace(); 
    
            } 
    
            System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); 
    
        } 
    
    } 
    
    

    其运行结果如下:

    
    main主线程运行开始!
    
    A 线程运行开始!
    
    子线程A运行 : 0
    
    B 线程运行开始!
    
    子线程B运行 : 0
    
    子线程A运行 : 1
    
    子线程B运行 : 1
    
    子线程A运行 : 2
    
    子线程B运行 : 2
    
    子线程A运行 : 3
    
    子线程B运行 : 3
    
    子线程A运行 : 4
    
    子线程B运行 : 4
    
    A 线程运行结束!
    
    B 线程运行结束!
    
    main主线程运行结束!
    
    

    主线程一定会等子线程都结束了才结束。

    4.6 public static void yield()

    暂停当前正在执行的线程对象,并执行其他线程。该方法是个静态方法。让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

    例子如下:

    
    class ThreadYield extends Thread{
    
        public ThreadYield(String name) { 
    
            super(name); 
    
        } 
    
        @Override 
    
        public void run() { 
    
            for (int i = 1; i <= 50; i++) { 
    
                System.out.println("" + this.getName() + "-----" + i); 
    
                // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行) 
    
                if (i ==30) { 
    
                    this.yield(); 
    
                } 
    
            } 
    
    } 
    
    } 
    
    public class Main { 
    
        public static void main(String[] args) { 
    
            ThreadYield yt1 = new ThreadYield("张三"); 
    
            ThreadYield yt2 = new ThreadYield("李四"); 
    
            yt1.start(); 
    
            yt2.start(); 
    
        } 
    
    } 
    
    

    上述代码会有两种运行结果:

    第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。

    第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

    4.7 public static void sleep(long millisec)

    在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的。sleep 方法允许较低优先级的线程获得运行机会。sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

    4.8 public void interrupt()/public static boolean interrupted()/public boolean isInterrupted()

    一个线程在未正常结束之前, 被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果比如会带着自己所持有的锁而永远的休眠,迟迟不归还锁等。 所以Thread.suspend, Thread.stop等方法都被Deprecated了。不能直接把一个线程搞挂掉, 但有时候又有必要让一个线程死掉, 或者让它结束某种等待的状态 该怎么办呢?一个比较优雅而安全的做法是:使用等待/通知机制或者给那个线程一个中断信号, 让它自己决定该怎么办。⑧中的三个方法就是和其相关的三个核心方法。中断通过调用Thread.interrupt()方法来实现. 这个方法通过修改了被调用线程的中断状态来告知那个线程, 说它被中断了. 对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true; 对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为true.但调用Thread.interrupted()会对中断状态进行复位。

    非阻塞中线程例子:

    
    public class MyThread extends Thread{
    
        public void run(){ 
    
            while(true){ 
    
                if(Thread.currentThread().isInterrupted()){ 
    
                    System.out.println("Someone interrupted me."); 
    
                } 
    
                else{ 
    
                    System.out.println("Thread is Going..."); 
    
                }
    
            } 
    
        } 
    
        public static void main(String[] args) throws InterruptedException { 
    
            MyThread t = new MyThread(); 
    
            t.start(); 
    
            Thread.sleep(3000); 
    
            t.interrupt(); 
    
        } 
    
    } 
    
    

    在main线程sleep的过程中由于t线程中isInterrupted()为false所以不断的输出”Thread is going”。当调用t线程的interrupt()后t线程中isInterrupted()为true。此时会输出Someone interrupted me.而且线程并不会因为中断信号而停止运行。因为它只是被修改一个中断信号而已。当我们调用t.interrput()的时候,线程t的中断状态(interrupted status) 会被置位。我们可以通过Thread.currentThread().isInterrupted() 来检查这个布尔型的中断状态。interrupt中断的是线程的某一部分业务逻辑,前提是线程需要检查自己的中断状态(isInterrupted())。

    如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:

    ① 如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException。

    ② 如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException。

    ③ 如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。

    如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。

    通过上面的分析,调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。interrupte方法的调用,该方法可在需要中断的线程本身中调用,也可在其他线程中调用需要中断的线程对象的该方法。综合考虑两种情况的run例子如下

    
    public void run() {
    
                try { 
    
                    while (true){ 
    
                        Thread.sleep(1000l);//阻塞状态,线程被调用了interrupte()方法,清除中断标志,抛出InterruptedException 
    
                        //dosomething 
    
                        boolean isIn = this.isInterrupted(); 
    
                        //运行状态,线程被调用了interrupte()方法,中断标志被设置为true 
    
                        //非阻塞状态中进行中断线程操作 
    
                        if(isInterrupted()) break;//退出循环,中断进程 
    
                    } 
    
                }catch (InterruptedException e){//阻塞状态中进行中断线程操作 
    
                    boolean isIn = this.isInterrupted();//退出阻塞状态,且中断标志被清除,重新设置为false,所以此处的isIn为false 
    
                    return;//退出run方法,中断进程 
    
                } 
    
            } 
    
    

    4.9 public final boolean isAlive()

    测试线程是否处于活动状态。

    4.10 public static Thread currentThread()

    返回对当前正在执行的线程对象的引用。

    5、Object 和线程相关方法

    Object 和线程相关方法主要有三个方法:wait(), notify(), notifyAll()。这几个方法经常与synchronized搭配使用,即在synchronized修饰的同步代码块或方法里面调用wait() 与 notify/notifyAll()方法。关于synchronized在下面会详细介绍。

    由于 wait() 与 notify/notifyAll() 是放在同步代码块中的,因此线程在执行它们时,肯定是进入了临界区中的,即该线程肯定是获得了锁的,如果在执行wait() 与 notify/notifyAll() 之前没有获得相应的对象锁,就会抛出:java.lang.IllegalMonitorStateException异常。。当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁 的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。从这里可以看出,notify/notifyAll()执行后,并不立即释放锁,而是要等到执行完临界区中代码后,再释放。在实际编程中,应该尽量在线程调用notify/notifyAll()后,立即退出临界区。即不要在notify/notifyAll()后面再写一些耗时的代码。假设在线程A中执行wait(),在线程B中执行notify()。但如果线程B先执行了notify()然后结束了,线程A才去执行wait(),那此时,线程A将无法被正常唤醒了。下面是一个经典的例子,通过调用Object的wait(),notify()实现建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。

    
    public class MyThreadPrinter implements Runnable {
    
        private String name;   
    
        private Object prev;   
    
        private Object self;   
    
        private MyThreadPrinter(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.print(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();   
    
            MyThreadPrinter pa = new MyThreadPrinter("A", c, a);   
    
            MyThreadPrinter pb = new MyThreadPrinter("B", a, b);   
    
            MyThreadPrinter pc = new MyThreadPrinter("C", b, c);   
    
            new Thread(pa).start(); 
    
            Thread.sleep(100);  //确保按顺序A、B、C执行 
    
            new Thread(pb).start(); 
    
            Thread.sleep(100);   
    
            new Thread(pc).start();   
    
            Thread.sleep(100);   
    
            }   
    
    }   
    
    

    解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。

    6、synchronized

    6.1 修饰实例方法

    作用于当前实例加锁,进入同步代码前要获得当前实例的锁。所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,例如:

    
    public class AccountingSync implements Runnable{
    
        //共享资源(临界资源)
    
        static int i=0;
    
        /**
    
        * synchronized 修饰实例方法
    
        */
    
        public synchronized void increase(){
    
            i++;
    
        }
    
        @Override
    
        public void run() {
    
            for(int j=0;j<1000000;j++){
    
                increase();
    
            }
    
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            AccountingSync instance=new AccountingSync();
    
            Thread t1=new Thread(instance);
    
            Thread t2=new Thread(instance);
    
            t1.start();
    
            t2.start();
    
            t1.join();
    
            t2.join();
    
            System.out.println(i);
    
        }
    
        /**
    
        * 输出结果:
    
        * 2000000
    
        */
    
    }
    
    

    上述代码中,开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象:

    
    public class AccountingSyncBad implements Runnable{
    
        static int i=0;
    
        public synchronized void increase(){
    
            i++;
    
        }
    
        @Override
    
        public void run() {
    
            for(int j=0;j<1000000;j++){
    
                increase();
    
            }
    
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            //new新实例
    
            Thread t1=new Thread(new AccountingSyncBad());
    
            //new新实例
    
            Thread t2=new Thread(new AccountingSyncBad());
    
            t1.start();
    
            t2.start();
    
            //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
    
            t1.join();
    
            t2.join();
    
            System.out.println(i);
    
        }
    
    }
    
    

    上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。

    6.2 修饰静态方法

    当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码:

    
    public class AccountingSyncClass implements Runnable{
    
        static int i=0;
    
        /**
    
        * 作用于静态方法,锁是当前class对象,也就是
    
        * AccountingSyncClass类对应的class对象
    
        */
    
        public static synchronized void increase(){
    
            i++;
    
        }
    
        /**
    
        * 非静态,访问时锁不一样不会发生互斥
    
        */
    
        public synchronized void increase4Obj(){
    
            i++;
    
        }
    
        @Override
    
        public void run() {
    
            for(int j=0;j<1000000;j++){
    
                increase();
    
            }
    
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            //new新实例
    
            Thread t1=new Thread(new AccountingSyncClass());
    
            //new心事了
    
            Thread t2=new Thread(new AccountingSyncClass());
    
            //启动线程
    
            t1.start();t2.start();
    
            t1.join();t2.join();
    
            System.out.println(i);
    
        }
    
    }
    
    

    由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

    6.3 修饰代码块

    除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

    
    public class AccountingSync implements Runnable{
    
        static AccountingSync instance=new AccountingSync();
    
        static int i=0;
    
        @Override
    
        public void run() {
    
            //省略其他耗时操作....
    
            //使用同步代码块对变量i进行同步操作,锁对象为instance
    
            synchronized(instance){
    
                for(int j=0;j<1000000;j++){
    
                        i++;
    
                  }
    
            }
    
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1=new Thread(instance);
    
            Thread t2=new Thread(instance);
    
            t1.start();t2.start();
    
            t1.join();t2.join();
    
            System.out.println(i);
    
        }
    
    }
    
    

    从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

    
    //this,当前实例对象锁
    
    synchronized(this){
    
        for(int j=0;j<1000000;j++){
    
            i++;
    
        }
    
    }
    
    //class对象锁
    
    synchronized(AccountingSync.class){
    
        for(int j=0;j<1000000;j++){
    
            i++;
    
        }
    
    }
    
    

    相关文章

      网友评论

          本文标题:Java 多线程

          本文链接:https://www.haomeiwen.com/subject/sqltjftx.html