java基础教程11:线程和线程间的同步
2016-07-09 21:20
591 查看
零、实现多线程的两种方式
(1)继承thread
下面几乎所有例子都采用这种办法
(2)实现runnable接口(比如说该类已经继承了其他类)
在runable中,对象的数据是共享的。
一、使用synchronized
首先是一个简单的多线程程序的实现
如果要求上面的i是static变量,程序就可能会出现一个数字被打印了多次。这是因为前一个线程还没有完成对i的修改,后面的线程已经进入了对i值的打印。Java中使用synchronized保证一段代码在多线程执行时是互斥的。
将代码中的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 ),指的是外层函数获得锁的时候,内层递归函数仍然有获取该锁的代码
如下面代码所示
如上所示,如果锁不可重入,那么,在第二次加锁的时候,程序就会一直等待发生死锁。
除了自旋锁外,java1.6中新加入了偏向锁。主要用于解决无竞争下的锁性能问题。偏向锁的想法是,在上面锁重入(或者相同线程继续需要上次释放的锁时)的时候,无需验证,让监视对象偏向于这个线程,避免了多次没有意义的CAS操作。(将在lock中讲解CAS的基本操作)。当然,偏向锁也会带来问题,如果有竞争的情况下,偏向锁释放会带来性能问题。
综上,synchronized 这种机制存在下列问题
(1)加锁释放锁的性能问题
(2)一个线程持有该锁会导致需要此锁的线程被挂起
(3)优先级高的线程可能会等待优先级低的线程释放锁,引起优先级倒置
二、lock
不同于synchronized是一个关键字,lock则是一个类(实际上是一个接口)
最常见的用法如下
在 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队列中进行等待的锁显得不公平。也就是非公平锁的由来。
示例代码如下
以上两者有什么区别的?
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()这样的语句写在当前上下文中。
典型的操作代码如下
代码condition用来判定线程被唤醒后是否执行还是继续wait。当然,wait可能会抛出异常,所以异常处理也是必要的。不然不能通过编译。
wait的内部实现为
wait首先释放被synchronized锁定的对象锁,然后循环等待条件为真,如果为真,则加锁后继续执行。obj.notify()/notifyAll()则是负责将这个条件设置为真而已。完整的使用参考如下实例
中间的延时3秒是必须的。不然,obj.notifyAll()时就没有正在wait的线程了。
(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的线程了。
相关文章推荐
- RxJava 并发之数据流发射太快如何办
- 如何理解和使用Java package包
- eclipse快捷键大全
- JavaBean技术初识
- java jdbc 连接mysql数据库 实现增删改查
- Java字符串解析
- spring配置文件 --可恶的命名空间
- Java谜题心得
- 第一个Java程序 & 基本语法
- java实现qq邮箱每天定时发送邮件
- 类与对象
- Java学习-标识符-变量-基本类型-算术运算符
- 字符相关
- java Socket实现Web服务器
- java安全性和数据库设计注意事项
- java后台调用 SAP RFC 第二种方法
- Java服务器热部署的实现原理
- java 获取昨天的日期
- java枚举
- 不用算术运算符实现两个数的加法(按位异或)