您的位置:首页 > 编程语言 > Java开发

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

2016-07-09 21:20 591 查看
零、实现多线程的两种方式

(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中,对象的数据是共享的。

一、使用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的线程了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: