java基础教程11:线程和线程间的同步

来源:互联网 发布:动态面板数据是 编辑:程序博客网 时间:2024/06/08 13:06

零、实现多线程的两种方式
(1)继承thread
下面几乎所有例子都采用这种办法
(2)实现runnable接口(比如说该类已经继承了其他类)

package Pack1;public class MyThread implements Runnable{    private String name;    private int i;    public MyThread(String name){        this.name = name;        i=0;    }    public void run(){        i++;        System.out.println(name+":"+i);               }    public static void main(String[] args) {        MyThread t1 =new MyThread("t1");        new Thread(t1).start();        new Thread(t1).start();        new Thread(t1).start();        System.out.println("Done");    }}

在runable中,对象的数据是共享的。比如说上面的MyThread.name就是共享的。

特别需要注意的一点是,在java中,必须调用start才能启动线程。如果调用了run就跟不同函数的调用是一样的。

一、使用synchronized
首先是一个简单的多线程程序的实现

package Pack1;public class MyThread extends Thread{    private String name;    private int i;    public MyThread(String name){        this.name = name;        i=0;    }    public void run(){        while(i<=10){            System.out.println(name + ":" + i);            i++;        }    }    public static void main(String[] args){        MyThread t1 = new MyThread("t1");        MyThread t2 = new MyThread("t2");        t1.start();        t2.start();    }}

如果要求上面的i是static变量,程序就可能会出现一个数字被打印了多次。这是因为前一个线程还没有完成对i的修改,后面的线程已经进入了对i值的打印。Java中使用synchronized保证一段代码在多线程执行时是互斥的。

package Pack1;public class MyThread extends Thread{    private String name;    private static int i;    private static Object obj;    public MyThread(String name){        this.name = name;        i=0;        obj = new Object();    }    public void run(){        synchronized (obj){            while(i<=10){                System.out.println(name + ":" + i);                System.out.flush();                i++;            }        }    }    public static void main(String[] args){        MyThread t1 = new MyThread("t1");        MyThread t2 = new MyThread("t2");        t1.start();        t2.start();    }}

将代码中的synchronized (obj)改为常见的synchronized (this)可不可以呢?答案是不行的!因为,此时,两个thread是两个对象,对this加锁是互不干扰的,不能形成互斥。所谓加锁,就是程序在synchronized (obj)就会试图向对象加一个锁,如果不能加锁则会等待。相比而言,lock功能更为强大一点。当synchronized关键字作用于方法时,锁定的对象其实为this。所一上述代码取消掉 synchronized (obj)而将互斥代码扔在一个synchronized 修饰的方法中也不能实现互斥。
sychronized的对象最好选择引用不会变化的对象,比如说用final修饰。另外,synchronized锁限制的代码段要尽可能小来提升性能。
synchronized的实现原理是对象监视器(也就是我们常说的锁)
这里写图片描述
Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List。目的是为了降低线程的出列速度。
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程
ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通 过pthread_mutex_lock函数)。线程被阻塞后就进入了内核调度状态,导致操作系统在用户态和内核态之间来回变化。所以又新加入了一种机制————自旋。线程不进入阻塞状态,而是执行空指令,此时线程占着CPU不放,争取获得锁的机会。显然,自旋周期是一个需要权衡的量。
现在,锁一般都是可重入的( ReentrantLock 和synchronized ),指的是外层函数获得锁的时候,内层递归函数仍然有获取该锁的代码
如下面代码所示

public class Test implements Runnable{ public synchronized void get(){   System.out.println(Thread.currentThread().getId());   set();  } public synchronized void set(){   System.out.println(Thread.currentThread().getId());  } @Override  public void run() {   get();  }  public static void main(String[] args) {   Test ss=new Test();   new Thread(ss).start();   new Thread(ss).start();   new Thread(ss).start();  } }

如上所示,如果锁不可重入,那么,在第二次加锁的时候,程序就会一直等待发生死锁。
除了自旋锁外,java1.6中新加入了偏向锁。主要用于解决无竞争下的锁性能问题。偏向锁的想法是,在上面锁重入(或者相同线程继续需要上次释放的锁时)的时候,无需验证,让监视对象偏向于这个线程,避免了多次没有意义的CAS操作。(将在lock中讲解CAS的基本操作)。当然,偏向锁也会带来问题,如果有竞争的情况下,偏向锁释放会带来性能问题。

综上,synchronized 这种机制存在下列问题
(1)加锁释放锁的性能问题
(2)一个线程持有该锁会导致需要此锁的线程被挂起
(3)优先级高的线程可能会等待优先级低的线程释放锁,引起优先级倒置

二、lock
不同于synchronized是一个关键字,lock则是一个类(实际上是一个接口)

public interface Lock {    void lock();    void lockInterruptibly() throws InterruptedException;    boolean tryLock();    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    void unlock();    Condition newCondition();}

最常见的用法如下

package Pack1;import java.util.concurrent.locks.*;public class MyThread extends Thread{    private String name;    private static Integer i;    private static Lock lock;    public MyThread(String name){        this.name = name;        i=0;        lock = new ReentrantLock();    }    public void run(){        while(i<=10){            lock.lock();            System.out.println(name + ":" + i);            System.out.flush();            i++;            lock.unlock();        }    }    public static void main(String[] args) {        MyThread t1 =new MyThread("t1");        MyThread t2 =new MyThread("t2");        t1.start();        t2.start();        System.out.println("hahah");    }}

在 java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、 ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer(AQS)类。
AQS中维护一个CHS队列(一个非阻塞的FIFO队列,也就是说调用者插入或者移除一个节点时,在并发条件下不会被阻塞,而是通过自旋锁和CAS)
(1)CAS就是一种乐观锁,每次并不加锁而是假设没有冲突的就去常识完成某项操作,如果冲突失败就重试,知道成功为止。整个J.U.C都是建立在CAS机制上的。实际上,CAS的原理可以用读取–>操作–>再次读取,检查数据有无变化–>若无变化对数据进行更改,有变化则重新尝试。显然,最后一步仍然可能出现问题,但是,CAS实际上是CPU提供的一个指令,所以,把这个问题丢给硬件工程师好了。

(2)volatile关键字
对于volatiile关键字,JVM只保证读取到的是内存中最新的值。没有同步的含义。即使用volatile标记了变量,多线程操作时仍然可能出现问题。

有CAS技术和volatile技术,我们就可以维持一个变量state,用于同步线程间的共享状态。显然,通过检测这个state,我们就可以对线程进行同步了
ReentrantLock主要提供lock和unlcok两个方法。lock默认是一种非公平锁(先到者不一定先得)。运行原理如图
这里写图片描述
在队列中等待的线程全部处于阻塞状态,在linux是通过pthread_mutex_lock函数把线程交给系统内核进行阻塞。如果有线程竞争锁的时候,他会首先尝试获得锁,这对于已经在CLH队列中进行等待的锁显得不公平。也就是非公平锁的由来。

示例代码如下

package Pack1;import java.util.concurrent.locks.*;public class MyThread extends Thread{    private String name;    private static Integer i;    private static Lock lock;    public MyThread(String name){        this.name = name;        i=0;        lock = new ReentrantLock();    }    public void run(){        while(i<=10){            lock.lock();            System.out.println(name + ":" + i);            System.out.flush();            i++;            lock.unlock();        }    }    public static void main(String[] args) {        MyThread t1 =new MyThread("t1");        MyThread t2 =new MyThread("t2");        t1.start();        t2.start();        System.out.println("hahah");    }}

以上两者有什么区别的?
AQS基于阻塞的CLH队列,对该队列的操作通过CAS完成,并且实现了偏向锁的功能,完全依靠系统阻塞挂起线程。但是更灵活
synchronized是一个基于CAS的等待队列,也实现了偏向锁,并可以依靠系统阻塞并同时实现了自旋锁,可根据不同系统硬件进行优化。

三、使用wait,notifyall,notify
在最原始的类——object中,有notify,notifyall方法和wait方法。都是final修饰的。
void notifyAll()
解除所有那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
void notify()
随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
void wait()

导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
void wait(long millis)和void wait(long millis,int nanos)
导致线程进入等待状态直到它被通知或者经过指定的时间。这些方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。

Object.wait()和Object.notify()和Object.notifyall()必须写在synchronized方法内部或者synchronized块内部,这是因为:这几个方法要求当前正在运行object.wait()方法的线程拥有object的对象锁。即使你确实知道当前上下文线程确实拥有了对象锁,也不能将object.wait(),notfiy()这样的语句写在当前上下文中。
典型的操作代码如下

public void test() throws InterruptedException {   synchronized(obj) {     while (! contidition) {       obj.wait();     }   } } 

代码condition用来判定线程被唤醒后是否执行还是继续wait。当然,wait可能会抛出异常,所以异常处理也是必要的。不然不能通过编译。

wait的内部实现为

    wait() {         unlock(mutex);//解锁mutex         wait_condition(condition);//等待内置条件变量condition         lock(mutex);//竞争锁     } 

wait首先释放被synchronized锁定的对象锁,然后循环等待条件为真,如果为真,则加锁后继续执行。obj.notify()/notifyAll()则是负责将这个条件设置为真而已。完整的使用参考如下实例

package Pack1;public class MyThread extends Thread{    private String name;    private static Integer i;    private static Object obj;    public MyThread(String name){        this.name = name;        i=0;        obj = new Object();    }    public void run(){        synchronized(obj){            try {                obj.wait();            } catch (InterruptedException e) {                // TODO Auto-generated catch block                e.printStackTrace();            }            System.out.println(name);           }    }    public static void main(String[] args) {        MyThread t1 =new MyThread("t1");        t1.start();        try {            sleep(3000);        } catch (InterruptedException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }        synchronized(obj){            obj.notifyAll();        }        System.out.println("Done");    }}

中间的延时3秒是必须的。不然,obj.notifyAll()时就没有正在wait的线程了。

0 0