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

零、实现多线程的两种方式

(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");
                }
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值