零、实现多线程的两种方式
(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");
}
}
(3)实现callable接口
callable接口需要和futuretask一起使用
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
* 一、创建执行线程的方式三:实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
*
* 二、执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。 FutureTask 是 Future 接口的实现类
*/
public class TestCallable {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
//1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
FutureTask<Integer> result = new FutureTask<>(td);
new Thread(result).start();
//2.接收线程运算后的结果
try {
Integer sum = result.get(); //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
System.out.println(sum);
System.out.println("------------------------------------");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class ThreadDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
return sum;
}
}
在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则是一个类(实际上是一个接口)。那么,为什么要有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的线程了。
四、Sleep Yield
和object.wait()不同,Thread类的sleep和yield都不会释放自己持有的锁。yield是暂时释放cpu,看看是否有别的的线程来抢占,并立即进入就绪状态。sleep则是在等待一定时间后在进入就绪状态。
五、一个死锁的实例
public class SandBox {
public static void main(String args[]){
System.out.println("test begin");
DieLock dl1 = new DieLock(true,"A");
DieLock dl2 = new DieLock(false,"B");
dl1.start();
dl2.start();
}
}
class DieLock extends Thread {
private boolean flag;
private String threadName;
private static Object objA = new Object();
private static Object objB = new Object();
public DieLock(boolean flag,String threadName) {
this.flag = flag;
this.threadName = threadName;
}
@Override
public void run() {
if (flag) {
synchronized (objA) {
System.out.println(threadName+"objA");
/*
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
synchronized (objB) {
System.out.println(threadName+"objB");
}
}
} else {
synchronized (objB) {
System.out.println(threadName+"objB");
/*
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
synchronized (objA) {
System.out.println(threadName+"objA");
}
}
}
}
}