Java中的线程同步

    技术2022-05-11  72

     

    理解线程的同步

     

        多线程,能够让CPU同时处理多个任务,其实并不是同一时间,而是CPU使用了时间片轮选机制,而且速度很快,我们感觉起来好像是多个任务同时进行一样。

        Java中,能通过两种方法使用线程。一是继承Thread类,二是实现Runnable接口。具体怎么使用,请参考相应的资料。该文档主要介绍的是多线程的同步问题。

     

        我们能够创建许许多多的线程,来处理许许多多的事情。但线程的运行是不确定的,我们无法知道哪个线程会先被CPU执行,设置优先权也不能解决,因为CPU只要有一点点地空闲,它都会去执行其他的线程,高优先级线程只是抢资源比较狠一点而已。由于线程的不确定性,使得当多个线程同时对某一对象进行操作的时候,便有可能出现不可预知的情况。举个例子,当某个线程对一个文件进行操作,比如是往文件里写数据,而另一个线程又对该文件进行写数据,那最后这个文件里到底是什么数据呢?由于线程的不确定性,所以我们无法预知结果。怎么解决线程的不确定性呢?嗯,对了,线程的同步(synchronized),能使线程按照你自己的意愿,先处理什么,后处理什么。

     

        在讲线程的同步之前,先讲讲同步线程(需要同步的线程)的几种状态吧。休眠状态、等待状态和执行状态是同步线程所特有的,其他的一些比如新状态、阻塞状态、死亡状态等是所有线程都有的,在这里不再讨论。

        休眠状态:所有在同步代码中调用wait方法的线程都会进入该状态,在没有被唤醒或没到达休眠结束时间前,状态是不会改变的。休眠前,线程会自动记录本身的一些状态,比如局部变量的值等(非局部的值是不保存的)。休眠状态的线程会存放在休眠池中,由系统管理。

        等待状态:被唤醒的或到达休眠结束时间的休眠线程,和进入同步代码(*1)的但没有得到钥匙(*2)的线程,都会进入等待状态。在没有得到钥匙前,线程的状态是不会改变的。等待状态的线程会存放在等待池中,由系统管理。只有在等待池中的线程,才有资格抢夺钥匙。

        运行状态:即正在运行的线程。运行状态的线程,能够调用wait方法,让出钥匙,使自己休眠,进入到休眠池中,并会记录当前的运行状态,以便下次夺取钥匙的时候,能继续执行休眠前的动作。

     

    注解:(*1:同步代码即同步方法或者同步块中的代码)

        *2:这里讲到的钥匙,即其他资料中所说的锁,得到锁的线程能够运行同步代码,使线程进入运行状态,本人觉得不太好理解,所以就换成个人觉得比较好理解的钥匙了)

     

    转入正题:先看一下下面的程序代码

    代码一:

    public class ThreadTest implements Runnable {

     

        public synchronized void run() {

           for (int i = 0; i < 10; i++) {

               System.out.print(" " + i);

               try {

                  Thread.sleep(5);

               } catch (InterruptedException e) {

                  e.printStackTrace();

               }

           }

        }

     

        public static void main(String[] args) {

           Runnable r1 = new ThreadTest();

           Runnable r2 = new ThreadTest();

           Thread t1 = new Thread(r1);

           Thread t2 = new Thread(r2);

           t1.start();

           t2.start();

        }

    }

    运行结果为:

    0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

     

    代码二:

    public class ThreadTest implements Runnable {

     

        public synchronized void run() {

           for (int i = 0; i < 10; i++) {

               System.out.print(" " + i);

               try {

                  Thread.sleep(5);

               } catch (InterruptedException e) {

                  e.printStackTrace();

               }

            }

        }

     

        public static void main(String[] args) {

           Runnable r = new ThreadTest();

           Thread t1 = new Thread(r);

           Thread t2 = new Thread(r);

           t1.start();

           t2.start();

        }

    }

    运行结果:

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

     

    两次的运行结果很显然不一样,代码一的同步方法好像没有起到作用,代码二的就起到作用了。为什么了?看清楚没有?对了,就是t1t2所指向的线程所包含的Runnable对象不一致引起的。代码一中,t1包含的是r1t2包含的是r2,显然r1r2不是同一个对象,而同步代码针对的是同一对象,所以在这里不钩成同步。代码二中,t1包含rt2也是包含r,两个线程处理同一个对象,所以同步方法便起了同步的作用,达到了程序的同步。

     

        嗯,很好!知道了线程的同步是针对同一对象来说的,但为什么会这样?OK,接下来我便讲讲线程同步的机制吧。

     

    再看一下下面的代码。

    代码三:

    public class ThreadTest implements Runnable {

     

        public synchronized void run() {

           String name = Thread.currentThread().getName();

           System.out.println(name + "抢到了钥匙,开始执行!");

           for (int i = 0; i < 10; i++) {

               System.out.print(i + " ");

               try {

                  Thread.sleep(5);

               } catch (InterruptedException e) {

                  e.printStackTrace();

               }

           }

           System.out.println("/n" + name + "运行结束,释放了钥匙!");

        }

     

        public static void main(String[] args) {

           Runnable r = new ThreadTest();

           Thread t1 = new Thread(r, "t1");

           Thread t2 = new Thread(r, "t2");

           Thread t3 = new Thread(r, "t3");

           t1.start();

           t2.start();

           t3.start();

        }

    }

     

    运行结果:

    t1抢到了钥匙,开始执行!

    0 1 2 3 4 5 6 7 8 9

    t1运行结束,释放了钥匙!

    t2抢到了钥匙,开始执行!

    0 1 2 3 4 5 6 7 8 9

    t2运行结束,释放了钥匙!

    t3抢到了钥匙,开始执行!

    0 1 2 3 4 5 6 7 8 9

    t3运行结束,释放了钥匙!

     

        看到上面的运行结果没?没错,每个对象都有且仅有一把钥匙,只有得到该钥匙的线程能够执行该对象的同步代码,那是不是同步代码也需要钥匙才能执行呢?“不是,非同步的我们不管,你想执行就执行吧,我们只管同步代码”,系统很明确的说。嗯,没错,其实可以这样理解,同步代码都是加了锁的代码,只有得到钥匙后,才能把锁打开,进入到同步代码中,没钥匙的只能在门口吃西北风。当得到钥匙的线程执行完任务后,便把钥匙归还给对象,站在门口的线程一看到钥匙归还了,就开始争夺钥匙,强到钥匙的线程,便能够把锁打开,执行任务了。对照一下上面所讲的线程状态,站在门口(等待池)等的,是等待状态线程,它们等待钥匙,并准备争夺钥匙;得到钥匙的就不用多说了吧,是运行状态的线程;那休眠状态的在哪?没错,它们太懒惰了,没有人叫醒他们,他们都在家(休眠池)里睡觉。

     

        上面都是用同步方法来解释线程的同步,现在开始用另一种同步代码同步块来解释线程的同步吧。先看下面的代码。

     

    代码四:

    public class ThreadTest implements Runnable {

     

        public void run() {

           String name = Thread.currentThread().getName();

           System.out.println(name + "的同步块之前");

           synchronized (this) {

               System.out.println(name + "的同步块内");

               for (int i = 0; i < 10; i++) {

                  System.out.print(i + " ");

                  try {

                      Thread.sleep(5);

                  } catch (InterruptedException e) {

                      e.printStackTrace();

                  }

               }

           }

           System.out.println("/n" + name + "同步块之后");

        }

     

        public static void main(String[] args) {

           Runnable r = new ThreadTest();

           Thread t1 = new Thread(r, "t1");

           Thread t2 = new Thread(r, "t2");

           Thread t3 = new Thread(r, "t3");

           t1.start();

           t2.start();

           t3.start();

        }

    }

     

    运行结果:

    t1的同步块之前

    t1的同步块内

    0 t2的同步块之前

    t3的同步块之前

    1 2 3 4 5 6 7 8 9 t2的同步块内

    0

    t1同步块之后

    1 2 3 4 5 6 7 8 9 t3的同步块内

    0

    t2同步块之后

    1 2 3 4 5 6 7 8 9

    t3同步块之后

     

           结果有问题啊,都乱掉了!不,结果没问题,因为同步块的作用就是使同步块里的代码进行同步,同步块外的不管。换句话说,就是同步块把同步块内的代码上锁了,没有得到钥匙的线程不能进入,只能在外面等待。你可能会问,为什么输出第一个01之间会输出“t2的同步块之前”,“t3的同步块之前”这两句话,不是进入到同步状态了吗?t1得到了钥匙,t2t3没有钥匙,为什么能照样执行呢?嗯,问得很好,请再看一下代码,打印的那行(灰底)在同步块之外,因此不受同步的约束。可以这样理解,那行打印的程序并没有上锁,因此并不需要得到钥匙才能执行,所有线程都能自由的执行。在没有遇到关键字synchronized之前,线程会一直运行下去的。当线程运行到关键字synchronized的时候,便会进入到等待池中,然后查看对象的钥匙是否没人使用,是的话就去争夺,夺取钥匙后便能进入上了锁的同步代码中,没有夺到就只能在等待池中等待。

           那同步块synchronized后面的this(红底)是什么意思呢?他的意思是:使用哪一个对象的对象管理器,来对同步块进行加锁呢?上述代码使用的是当前对象(this)的对象管理器来对同步块进行加锁,只能有得到当前对象(this)的钥匙才能够解锁,进入到同步块中。一个对象可以有多把锁,但只有一把钥匙,使用了这个对象的锁,就只能使用这个对象的钥匙来开锁。即这里的同步代码由这个对象的锁和钥匙来管理。同步方法没有明确的说明用哪个对象的对象管理器啊?嗯,java规定同步方法都只能由当前对象的对象管理器控制。

     

           上面都明白了的话,就来点提高吧,先看下面代码。

     

    代码五:(为了简便,我把要导入的包和异常处理都删掉了)

    public class OperatorFile {

        static Object obj;

        ReadThread rt;

        WriteThread wt;

        String fileName;

        boolean canRead;

     

        OperatorFile(String fileName) {

           this.fileName = fileName;

           obj = new Object();

           rt = new ReadThread();

           wt = new WriteThread();

           rt.start();

           wt.start();

        }

     

        public void readFile(String fileName) {

           synchronized (obj) {

               while (!canRead){

                   obj.wait();   //使当前线程进入休眠状态

    }

               FileInputStream fis = new FileInputStream(new File(fileName));

               byte[] data = new byte[fis.available()];

               fis.read(data);

               System.out.println(new String(data));

               fis.close();

           }

        }

     

        public void writeFile(String fileName) {

           synchronized (obj) {

               FileOutputStream fos = new FileOutputStream(fileName);

               for (int i = 'a'; i <= 'z'; i++) {

                  fos.write(i);

               }

               fos.close();

               canRead = true;

               obj.notify(); //唤醒在休眠池中的任意一个线程

           }

        }

     

        class ReadThread extends Thread {

           public void run() {

               readFile(fileName);

           }

        }

     

        class WriteThread extends Thread {

           public void run() {

               writeFile(fileName);

           }

        }

     

        public static void main(String[] args) {

           new OperatorFile("test.txt");

        }

    }

     

    运行结果:

    abcdefghijklmnopqrstuvwxyz

     

           常理来说,先调用rtstart方法,应该会先运行readFile去读文件,此时“test.txt”里没有内容,应该是什么也不会打印的,但为什么会先调用wtshart方法,执行writeFile,把a-z写到文件中,然后再把回到readFile去读文件呢?嗯,你猜得没错,就是因为readFile里有一个wait()方法,此方法使得当前的线程进入到休眠池中休眠,并且释放了obj对象的钥匙,因为同步块使用了obj的对象管理器进行对同步线程的管理。此时在等待池中等待的wt把空闲的钥匙夺了过来,所以能够把锁给打开,进入到同步块中,然后对文件进行写操作。注意,读和写是在不同的同步块中实现的,只要是属于同一对象的同步代码,便会给这些同步代码加上该对象的锁,由此可知,一个对象的锁可以有多个,但一个对象有且仅有一把钥匙。只有得到这把钥匙,才能随心所欲的打开任意一把锁。接下来writeFile把数据都写到文件中了,然后调用notify()方法,唤醒此对象管理器(这里是obj)中的一个线程,便把该线程从休眠池中移动到等待池中,rt线程被唤醒后,发觉钥匙空闲,因此把钥匙夺过来,然后继续上次运行,把文件的数据输出。

           代码五有几个值得注意的地方。什么?你看出来了?没错,第一个是synchronized所使用的对象管理器,这段代码是使用obj的对象管理器来管理同步线程的,因此需要在synchronized后面的括号中填上objobj是静态的?对,没错,我们的同步线程只能由一个对象来管理,多个对象是管理不了同步线程的,这里定义obj为静态是为了确保obj只有一份,从而达到我们的要求。

           第二个要注意的地方是:waitnotify这两个方法,必须是obj的方法,不能是其他对象的方法。你自己可以去改一下,用thiswaitnotify方法,一定会有异常产生。在java doc上是这样写的“如果当前的线程不是此对象监视器的所有者”时,调用waitnotifynotifyAll便会产生“IllegalMonitorStateException”异常。说简单一点,使用哪个对象的对象管理器去控制同步线程,就只能使用该对象的waitnotifynotifyAll来控制同步。

           第三个要注意的地方是:wait方法外面的while循环。为什么要用循环语句,并且要定义一个标示符来控制循环呢?这样做能够明确的指明所需唤醒的线程。例如现在有三个线程(ABC)在休眠,现在我想让在运行态中D线程来唤醒A线程,应该如何实现呢?使用一般的方法是实现不了的,只能按照上面所说的,循环语句结合标示符来实现。看一下下面的代码:

     

    代码六:

    public class Test {

     

        boolean isA;

        boolean isB;

        boolean isC;

       

        synchronized void doA(){

           while(!isA){

               wait();

           }

           //do someting......

        }

        synchronized void doB(){

           while(!isB){

               wait();

           }

           //do someting......

        }

        synchronized void doC(){

           while(!isC){

               wait();

           }

           //do someting......

        }

        synchronized void doD(){

           isA = true;

           notifyAll();

           //do someting......

        }

    }

     

    分析:当执行doD方法的时候,令isA = true,然后唤醒所有的线程。此时doA中,由于isAtrue,跳出循环,所以不会执行到wait方法,线程A能够开始执行。而doBdoC中,由于isBisC还是false,因此进入到循环体中,执行wait方法继续休眠,达到了目的。所以通常情况下都是在循环内做休眠动作,并用一标示符控制线程是否应该被唤醒。

     

           到此结束,可能和官方的解释略有不同,一切以官方的为准。以上是我对线程同步的一点点见解,希望对你们有所帮助,如果有不当之处,望能指出,万分感谢。

     

     

     

    作者:RaymondLin

    时间: 2007-1-1

     

    最新回复(0)