(三)线程控制--java 多线程编程的那点小事

来源:互联网 发布:金字塔外星人知乎 编辑:程序博客网 时间:2024/05/01 21:06


   线程的控制,所白了就是控制线程间的一些时序问题,下面开始讲可以控制线程时序的几个方法:


一、睡眠函数 sleep

   (1)Thread.sleep();

   (2)TimeUnit.XXX.sleep();

   睡眠函数在上一节结束的时候已经讲过了,这里就balabala了

二、等待函数 Join

   join 函数在逻辑上理解起来可能有些绕,其实都怪这个名字起的太烂。join 方法的api原文解释是:Waits for this thread to die;这样一来,就很好理解了。

   就是说谁调用了这个方法,那么当前线程就要等到它死才能获得cpu

   举个例子

   class A  extends Thread{

      ...

      b.join();  //b 是B类的一个实例

      do jobA;     

   }

   class B  extends Thread{

      do jobB;

   }

   a.start(); b.start();

  看这个例子,在线程A中,有一条语句:b.join(); 这句话就是告诉当前线程(线程a,因为线程a执行到了这句话)要等到a死了才能做工作jobA

  这样,运行起来的效果就是 jobB 先完成后,jobA 才会执行。


   另外要注意的是:线程A 要等待线程B ,那么线程A中必须要维护一个B线程的引用,这样才能在合适的时机等待B执行完。(从业务逻辑的角度看,线程A之所以要等待B很可能是要使用线程B的运算结果,A和B有关连,且是A要等B,就是说只有A知道要在什么时候哪个地方等B,所以A中必须要有一个B线程的引用)

  好了,join 方法就差不多了,下面贴上一个小demo,巩固下:

import java.util.concurrent.TimeUnit;public class JoinMethodTest {class Thread1 extends Thread{Thread2 t2;public void run(){try {System.out.println("Thread1 will wait for t2 awake!");t2.join();} catch (InterruptedException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}for( int i = 0; i < 5; i++){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}System.out.println("Thread1111 said :  "+i);}}public Thread1(Thread2 t2){this.t2 = t2;} }class Thread2 extends Thread{public void run(){try {System.out.println("Thread2 sleeped!");TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e1) {e1.printStackTrace();}System.out.println("Thread2 awaked!");}}public Thread1 getThread1(Thread2 t2){return  new Thread1(t2);}public Thread2 getThread2(){return  new Thread2();}public static void main(String args[]) throws InterruptedException{JoinMethodTest jmt = new JoinMethodTest();Thread2 t2 = jmt.getThread2();Thread1 t1 = jmt.getThread1(t2);t2.start();t1.start();}}

结果:

Thread2 sleeped!Thread1 will wait for t2 awake!Thread2 awaked!Thread1111 said :  0Thread1111 said :  1Thread1111 said :  2Thread1111 said :  3Thread1111 said :  4

三、使用synchronized给临界区代码隐式的加锁(这里之所以成为隐式的加锁是因为你只要给方法或变量加了synchronized标志,那么线程并发时的同步互斥控制都有系统来帮你完成了)

  java 对防止资源冲突提供了内在支持,既synchronized。

  学习synchronized 要注意一下几点:

  1、如果某个线程调用了某个synchronized标志的方法,那么在这个线程从该方法返回前,其他所有要调用synchronized方法的线程都会阻塞,注意千万不要想成是其他线程暂时不执行该方法,先执行其他方法。

  2、每个对象有且仅有一把锁,如果一个对象同时有多个synchronized方法,那么当其中一个方法获得锁时,其他方法都不能被调用,或者说调用其他方法的线程都会阻塞。


  示例:

public class Worker {public synchronized void printEvenNumber(String tag){for(int i = 0 ; i < 100;i +=2){System.out.println(tag+" is printing even number : "+i);}}public  synchronized void printOddNumber(String tag){for(int i = 1 ; i < 100 ; i++){if( i % 2 != 0){System.out.println(tag+" is printing odd Number : "+i);}}}}


public class Thread1 extends Thread{private Worker w;private String curThreadName;public void run(){w.printEvenNumber(curThreadName);//w.printOddNumber(curThreadName);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}public Thread1(Worker w,String name){this.w = w;this.curThreadName = name;}}


import java.util.concurrent.TimeUnit;public class Thread2  extends Thread{private Worker w;private String curThreadName;public void run(){//w.printEvenNumber(curThreadName);w.printOddNumber(curThreadName);try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}}public Thread2(Worker w,String name){this.w = w;this.curThreadName = name;}}

  太懒了,可以在Thread1 和Thread2 中注释或添加 printEvenNumber() 和 printOddNumber方法来测试上面讲的2个注意;


四、使用Lock 来显式的给临界资源加锁

  Lock 这个东西就像我们在操作系统学信号量时的一个互斥信号量。使用lock.lock () 来给临界区加锁。使用Lock.unlock()来给临界区解锁.

  使用 lock 来进行临界资源的控制和使用synchronized的区别是:

   1、如果在调用synchronized标志的方法时在方法内出现了一个异常,此时你是没有办法做任何清理工作的。但使用显示的lock上锁再配上try+finally  你就可以去处理出现的异常,从而将系统维护在正确的状态了。

   2、使用lock来上锁,你还可以获取更细粒度的控制力。

五、使用synchronized构成同步控制块来保护临界区

  这种方法效果是和lock一样的,也可以获取耕细粒度的控制力,且代码更加简洁

  格式: 

  synchronized(synObj)

  {

      ....临界区代码

  }

  临界区代码是需要上锁的,前面已经讲过只有对象有且仅有一把锁。所以临界区要上锁必须要从某个对象获得,所以同步控制块的构成还需要一个synObj对象。

  eg:

  

synchronized(this)  //借用对象的锁来构成同步控制块{   i++;   i++;}

六、使用 volatile 关键字来保证事物的原子性

  实际上,volatile并不总能保证事物的原子性,先看个例子:

public class Counter {     public volatile static int count = 0;     public static void inc() {         //这里延迟1毫秒,使得结果明显        try {            Thread.sleep(1);        } catch (InterruptedException e) {        }         count++;    }     public static void main(String[] args) {         //同时启动1000个线程,去进行i++计算,看看实际结果         for (int i = 0; i < 1000; i++) {            new Thread(new Runnable() {                @Override                public void run() {                    Counter.inc();                }            }).start();        }         //这里每次运行的值都有可能不同,可能为1000        System.out.println("运行结果:Counter.count=" + Counter.count);    }}

运行结果:

运行结果:Counter.count=981

  原因就是:对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。

  解释这句话就要分析线程运行时的内存问题了。下面的内容摘自网络:

 

   每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这些交互

java volatile1

    如果,当线程把count变量的值从主内存拷贝到自己的线程栈之后,线程对该变量的所有操作都是对这个临时副本的操作,出现上面例子中运行结果:Counter.count=981 这个结果就是,有20个线程同时在自己的本地栈中对同一count值进行了修改,这样写回到主内存后当然结果是不会递增的。