进程java基础技术文

Java-Review-Note——4.多线程

2016-10-11  本文已影响271人  coder_pig

Java-Review-Note——4.多线程

标签: JavaStudy


PS:本来是分开三篇的,后来想想还是整理成一篇了,多线程看得够呛,只能说,很多东西还需要实践...
Σ(⊙▽⊙"a

程序,进程,线程及多线程的理解


线程的生命周期

注意:可以调isAlive()判断线程是否死亡,如果对已死亡的线程调start()方法会抛出
IllegalThreadStateException异常!


创建线程的三种方式


1.继承Thread类创建

流程:继承Thread类,重写run()方法,实例化线程对象,调start()方法从而启动run()方法。

示例代码

class MyThread extends Thread {
    public MyThread() {
        super("MyThread");  //标识进程名
        System.out.println("新建了线程:" + getName()); //getName()方法可获得线程名
    }
    public void run() { 
        for(int i = 1;i <= 3;i++) {
            System.out.println(Thread.currentThread().getName() +" : " + i );  
            try {  sleep(1000);  } catch (InterruptedException e) {  e.printStackTrace();  }  
        }
    }
}
//测试类
public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        MyThread mt = new MyThread();
        mt.start();
        //这里演示的是线程运行完猴才打印主线程中的语句,join()让异步执行的线程
        //改成同步执行,直到这个线程退出,程序才会继续执行
        try { mt.join(); }catch(InterruptedException e){ e.printStackTrace(); }
        System.out.println(t.getName() + " 打印完毕!");
    }
}

运行结果


2.实现Runnable接口创建

流程:实现Runnable接口,覆盖run()方法,实例化自定义线程类对象,实例化Thread对象,
将自定义线程对象作为参数传给Thread对象,调用Thread对象的start()方法启动run()方法。

示例代码

class MyThread implements Runnable {
    public void run() {
        for(int i = 1;i <= 3;i++) {
            System.out.println(Thread.currentThread().getName() +" : " + i );
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyThread(),"MyThread");
        thread.start();
        System.out.println(Thread.currentThread().getName() + " 打印完毕!");
    }
}

运行结果


3.实现Callable泛型接口创建

流程:实现Callable<T>泛型接口,重写call()方法,然后使用Future或FutureTask来创建。
简述:Callable和Future是jdk 1.5后引入的,引入目的是:解决两种普通创建
Thread无法直接获取执行结果的缺陷;Callable接口中只声明了一个call()的方法,
返回类型就是Callable传进来的V类型,一般情况下是配合ExecutorService来使用。
Future接口是对于具体的Runnable或Callable任务的执行结果进行取消(cancel),
查询是否取消成功(isCancelled),查询是否完成(isDone),获取查询结果(get)
该方法会阻塞直到任务返回结果,另外get还有一个方法get(long timeout, TimeUnit unit),
如果在指定时间内没获取到结果,就会返回null,也就是说Future提供三种功能:
判断任务是否完成,能否中断任务,能够获取任务的执行结果。
因为Future是一个接口,无法直接用来创建对象,因此有了FutureTask。
FutureTask,实现了RunnableFuture<V>接口,而RunnableFuture<V>接口接口继承了
Runnable和Future<V>接口,所以FutureTask既可以作为Runnable被线程执行,又可以作为
Future得到Callable的返回值。

使用示例

实现Callable接口

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程正在计算...");
        int sum = 0;
        for(int i = 0;i < 100;i++) { sum += i; }
        return sum;
    }
}

Future :

    ExecutorService executor = Executors.newCachedThreadPool();
    MyThread myThread = new MyThread();
    Future<Integer> result = executor.submit(myThread);
    executor.shutdown();    //如果不调shutdown方法,executor会一直等待运行,即使没线程
    try { 
        System.out.println("result运行结果:" + result.get()); 
        } catch (InterruptedException e) { e.printStackTrace();
        } catch (ExecutionException e) { e.printStackTrace(); 
    }

FutureTask

    //第一种ExecutorService
    ExecutorService executor = Executors.newCachedThreadPool();
    MyThread thread = new MyThread();
    FutureTask<Integer> result = new FutureTask<>(thread);
    executor.submit(result);
    executor.shutdown();
    //第二种Thread
    MyThread myThread = new MyThread();
    FutureTask<Integer> futureTask = new FutureTask<>(myThread);
    Thread thread = new Thread(futureTask);
    thread.start();
    try {
        System.out.println("result运行结果:" + result.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

运行结果


多线程集锦

1.线程执行的顺序

多个线程同时执行又叫线程并发,如果当前有多个线程在并发执行的话,多次运行
的结果可能是不唯一的,Java对于线程启动后唯一能保证的就是:
每个线程都被启动并且结束,但对于哪个线程先执行,何时执行,都没保证。

2.线程的优先级

操作系统中的线程是具有优先级,Java运行环境采用的是固定优先级调度算法
(Fixed Priority Scheduling)某一时刻有多个线程在运行,JVM会选择优先级最高
的线程运行,优先级较高的线程会抢占cpu时间,相同优先级的线程可能顺序执行,
也可能分时执行,取决于本地操作系统的线程调度策略,虽然说在任意时刻,应该
是最高优先级的线程在运行,但是这样是不能保证的,调度程序有可能选择优先级
较低的线程以避免饥饿(starvation)抢占(pre-empt)策略
当一个优先级更高的进程进行可运行状态时发生抢占,终止当正在运行的进程而立即
去执行优先级更高的线程,而两个相同优先级的线程则采用循环执行策略(round-robin);

3.Java中的线程优先级

Java中线程的优先级从0-10,整数,数值越大,优先级越大,默认线程优先级为5,
可调用:setPriority()改变线程优先级,或调用:getPriority()获得线程的优先级,
另外Java还提供了几个线程优先级的字段供使用:
MIN_PRIORITY(最低优先级0);MAX_PRIORITY(最高优先级10);NORM_PRIORITY(默认优先级5)
另外,只能说优先级高的线程更有可能获得CPU,但也不能说优先级较低的线程就
永远最后执行,这不是绝对的,可以理解成设置进程优先级只是给系统提供一个参考

4.Java提供的进程协作相关的方法

使用优先级不能保证并发执行的线程顺序,但是Java也给我们提供了一些线程协作相关的方法:

Thread类中

Object类中

疑问:为何这三个方法在Object类中?
答:每个对象都拥有一个monitor(锁),让当前线程等待某个对象的锁,应该通过这个对象操作,
而不是用当前线程来操作,因为当前线程可能等待的事多个线程的锁,用线程来操作,会非常复杂。

几个不安全,不推荐的方法

Java 1.5 新增Condition接口

用来代替wait(),notify(),notifyAll()实现实现线程间的协作,使用await(),signal(),
signalAll()来实现线程间的协作更加安全高效;Condition依赖于Lock接口,调用Lock对象的
newCondition()可以生成Condition,Condition的await(),signal()和signalAll()方法调用,
需要在lock()和unlock()之间使用


5.线程同步安全问题

当有两个或以上线程在同一时刻访问操作同一资源,可能会带来一些问题,比如:
数据库表中不允许插入重复数据,线程1,2都得到了数据X,然后线程1,2同时查询了
数据库,发现没有数据X,接着两线程都往数据库中插入X,然后就GG了,这就是线程
安全问题,而这里的数据库资源我们又称为:临界资源(共享资源)

6.如何解决线程安全问题

基本所有并发模式在解决线程安全问题时,都采用"系列化访问临界资源"的方式,
就是:同一时刻,只能有一个线程访问临界资源,也成"同步互斥访问"。通常就是
在操作临界资源的代码前加一个锁,当有线程访问资源,获取这个所,其他线程无法
访问,只能等待(堵塞),等这个线程访问完临界资源,然后释放锁,供其他线程继续访问。
而Java中提供两种方案来实现同步互斥访问:synchronizedLock,等下详细讲。

7.与锁相关的特殊情况:死锁,饥饿与活锁

每个对象都拥有一个锁,当多个线程,操作涉及到多个锁,就可能会出现这三种情况:

死锁(DeadLock)

两个或以上进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,
如果无外力作用,他们将继续这样僵持下去;简单点说:两个人互相持有对方想要的资源,
然后每一方都不愿意放弃自己手上的资源,就一直那样僵持着。

死锁发生的条件

互斥条件(临界资源);
请求和保持条件(请求资源但不释放自己暂用的资源);
不剥夺条件(线程获得的资源只有线程使用完后自己释放,不能被其他线程剥夺);
环路等待条件:在死锁发生时,必然存在一个"进程-资源环形链",t1等t2,t2等t1;

如何避免死锁

破坏四个条件中的一个或多个条件,常见的预防方法有如下两种:
有序资源分配法:资源按某种规则统一编号,申请时必须按照升序申请:
1.属于同一类的资源要一次申请完;2.申请不同类资源按照一定的顺序申请。
银行家算法:就是检查申请者对资源的最大需求量,如果当前各类资源都可以满足的
申请者的请求,就满足申请者的请求,这样申请者就可很快完成其计算,然后释放它占用
的资源,从而保证了系统中的所有进程都能完成,所以可避免死锁的发生。
理论上能够非常有效的避免死锁,但从某种意义上说,缺乏使用价值,因为很少有进程
能够知道所需资源的最大值,而且进程数目也不是固定的,往往是不断变化的,
况且原本可用的资源也可能突然间变得不可用(比如打印机损坏)。

饥饿(starvation)与饿死(starve to death)

资源分配策略有可能是不公平的,即不能保证等待时间上界的存在,即使没有发生死锁,
某些进程可能因长时间的等待,对进程推进与相应带来明显影响,此时的进程就是
发生了进程饥饿(starvation),当饥饿达到一定程序即此时进程即使完成了任务也
没有实际意义时,此时称该进程被饿死(starve to death),典型的例子:
文件打印,采用短文件优先策略,如果短文件太多,长文件会一直推迟,那还打印个毛。

活锁(LiveLock)

特殊的饥饿,一系列进程轮询等待某个不可能为真的条件为真,此时进程不会进入blocked状态,
但会占用CPU资源,,活锁还有几率能自己解开,而死锁则无法自己解开。(例子:都觉得对方
优先级比自己搞,相互谦让,导致无法使用某资源),简单避免死锁的方法:先来先服务策略


8.守护线程

也叫后台线程,是一种为其他线程提供服务的一种线程;当虚拟机检测到没有用户进程可服务的
时候,就会退出当前应用程序的运行,比如JVM的gc(垃圾回收线程),当我们程序中不再有任何
Thread的时候,垃圾回收就没事做了,即使还有其他后台线程,但是此时的JVM还是会退出!
将一个线程设置为后台线程:setDaemon(boolean)
判断一个线程是否为后台线程:isDaemon()


9.线程并发的问题

开始的问题:临时数据存放在内存中,而CPU执行指令比内存读写快太多!
解决方法:CPU中使用高速缓存,将需要数据从内存复制一份到缓存中,运算时CPU直接读缓存,
运算结束后,再将缓存中的数据刷新回内存中。

又有问题:单线程倒没什么,多线程的话,切线程处于不同的CPU中,每个线程都拥有自己的
高速缓存,这样可能会出现缓存不一致的问题,比如内存中的i = 0,然后在两个线程中都执行
i = i + 1;分别在两个线程中执行,最后的结果可能是1,而不是2。
硬件层次的解决方案在总线加LOCK#锁的方式缓存一致性协议(Intel的MESI协议)

10.并发编程的三个概念

综上:想要保证并发程序能够正确执行,必须保持原子性,可见性,有序性,只有有一个没
保证,就有可能会导致运行不正确。

11.Java中对并发编程的保证与8条先行发生原则

Java内存模型规定所有变量都是存储在主存中,每个线程都有自己的工作内存(类似于前面的高速
缓存),线程对变量的所有操作必须在工作内存中,而不能直接对主存进行操作,且每个线程不能
访问其他线程的工作内存。

Java中语言本身对原子性,可见性,有序性提供了哪些保证?

8条happens-before原则(先行发生原则)


12.线程并发的经典问题:生产者消费者问题

问题概述

两个共享固定缓冲区大小的线程,生产者线程负责生产一定量的数据放入缓冲区,
而消费者线程则负责消耗缓冲区中的数据,关键问题是需要保证
1.缓冲区满的时候,生产者不再往缓冲区中填充数据
2.缓存区空的时候,消费者不在消耗缓冲区中的数据

可以用简单的两种套路来模拟这个问题:
synchronized + wait() + notify()方式 或 Lock + Condition接口的await()与signal()实现
等下细讲。


13.同步容器

Java集合容器中有四类:List(数组)Set(集合)Queue(队列)Map,前三个都继承Collection接口,
Map本身是一个接口。我们平常使用的ArrayList,LinkedList,HashMap这些容器都是非线程安全的,并发访问的时候可能会
有问题,然后Java给我们提供了两类的同步容器:

image_1aummto991fre1k4dnpv19mv3jk9.png-21.6kBimage_1aummto991fre1k4dnpv19mv3jk9.png-21.6kB

相比起非同步容器,同步容器因为锁的关系,性能会稍弱。
另外同步容器也不一定是安全的,只能保证每个时刻只能有一个线程在访问他,集合删元素遍历的例子,
有时为了保证线程安全,还需要在方法调用端做额外的同步措施!

另外,在对Vector等容器并发地进行迭代修改时,会报ConcurrentModificationException异常!
原因是:Iterator执行next方法时,调用checkForComodification()时expectedModCount与modCount不相等,
由于执行remove(),modCount每次循环值都会改变,而expectedModCount并没改变,所以抛异常!

解决方法


14.并发容器

同步容器因为使用Synchronized进行同步,执行读写都需要去获取锁,并发的时候效率较低,
Jdk 1.5新增的concurrent提供了这些并发容器:


15.阻塞队列

在解决生产者与消费者问题时,我们的套路一般是对容器加锁,然后判断容器中数据满和空的情况,
然后唤醒或者阻塞生产者或者消费者线程,有些麻烦,如果使用阻塞队列,我们就不用关心那么多,
阻塞队列会对访问线程产生堵塞,比如当地队列满了,此时生产者线程会被阻塞,直到消费者消费
了队列中的元素,被堵塞的线程会自动唤醒。其实就是把wait(),notify()这些集成到队列中实现。

几种主要的阻塞队列

同样是java.util.concurrent包下提供的若干个阻塞队列:

堵塞队列除了对非阻塞队列的下述五个方法进行了同步:

还提供了另外4个非常有用的方法:

阻塞队列适用于生产者-消费者问题,因为不需要再单独考虑同步和线程间通信的问题。


16.线程组

我们可以通过java.lang.ThreadGroup对线程进行组操作,每个线程都归属于某个线程组管理的一员。
在创建Thread实例时,如果没有制定线程组参数,则默认属于创建者线程所隶属的线程组,这种隶属
关系在创建新线程时指定,在线程的整个生命周期里都不能改变!比如我们在main()中创建的新线程,
这个线程属于main这个线程管理中的一员!

作用

简化对多个线程的管理,对若干线程同时操作,比如:调用线程组的方法设置所有线程的优先级,
调用线程组的犯法启动或堵塞组中所有线程等;其实,线程组的最重要意义是线程安全,Java默认
创建的线程都是属于系统线程组,而处于同一线程组中的线程可以互相修改对方数据,当如果在不
同的线程组中,那么就不能"跨线程组"修改数据,从一定程度上保证了数据的安全。

线程组提供的操作

常见用法

使用示例

public class ThreadGroupTest {
    public static void main(String[] args) {
        ThreadGroup tGroup = new ThreadGroup("自定义线程组");
        Thread t1 = new Thread(tGroup,"自定义线程1");
        Thread t2 = new Thread(tGroup,"自定义线程2");
        System.out.println("线程组的初始最大优先级:" + tGroup.getMaxPriority());
        System.out.println(t1.getName() + "的初始优先级:" + t1.getPriority());
        System.out.println(t2.getName() + "的初始优先级:" + t2.getPriority());
        t1.setPriority(9);  //设置t1的优先级为9
        tGroup.setMaxPriority(8);   //设置线程组的优先级为8
        System.out.println("线程组的新最大优先级:" + tGroup.getMaxPriority());
        System.out.println(t1.getName() + "的新优先级" + t1.getPriority());
        t2.setPriority(10); //设置t2的优先级为10
        System.out.println(t2.getName() + "的新优先级" + t2.getPriority());
        System.out.println(tGroup.toString());
    }
}

运行结果


17.线程池

引入

直接创建的线程在调用完start()方法结束后,线程就结束了,而线程的创建与结束都需要耗费
一定的系统时间,如果并发的线程数量很多的话,不停的创建和销毁线程会消耗大量的时间,
效率就有些低了,我们想让线程在执行完任务以后并不销毁,而是让他进入休眠状态,然后
继续执行其他任务,当需要用到这个线程再唤醒,Java中使用线程池可以达到这样的效果。

ThreadPoolExecutor类

继承AbstractExecutorService类,并提供四种构造方法(前三其实都是调用第四个进行初始化
工作的):

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { }

参数解析

线程池相关的几个类之间的关系

使用示例

MyThread.java

public class MyThread implements Runnable {
    private int threadNum;

    public MyThread(int threadNum) {
        this.threadNum = threadNum;
    }

    @Override
    public void run() {
        System.out.println("线程:" + threadNum + "开始执行...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:" + threadNum + "执行完毕...");
    }
}

ThreadTest.java 测试类:

public class ThreadTest {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200,
                TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(5));
        for (int i = 0; i < 15; i++) {
            MyThread myThread = new MyThread(i);
            executor.execute(myThread);
            System.out.println("线程池中线程数目:" + executor.getPoolSize() + " 等待执行的任务数目:" + executor.getQueue().size());
        }
        executor.shutdown();
    }
}

另外,Java文档中并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors提供的几个静态方法来创建线程池:

当然,如果这个几个线程池都满足不了你的话,你可以继承ThreadPoolExecutor类重写。


18.Timer和TimerTask

Timer是Jdk提供的定时器类,延时或者重复执行任务,使用时候会主线程外开启
单独的线程来执行定时任务,可以指定执行一次或重复多次。TimerTask是实现了
Runnable接口的抽象类,代表一个可以被Timer执行的任务。

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    public void run() { ... timer.cancle(); }
},延时时间)

终止Timer的几个方式

另外,schedule保证每次延迟间隔固定,而scheduleAtFixedRate则可能因为某个调度
时间太长,而缩短间隔的方式,保证下一次调度在预定时间执行,比如,每隔3s调度一次:
正常都是:0,3,6,9,假如第二次调度花了2s,前者会变成:0,3+2,8,11,而后者会压缩间隔,
保证调度时间,变成:0,3+2,6,9,另外还要注意Timer不保证任务执行的十分准确!


19.三个并发辅助类:CountDownLatch,CyclicBarrier和Semaphore

CountDownLatch(类似于计时器,比如有任务A,需等其他4个任务执行完毕才执行,就可以用上)
使用方法如下:

CountDownLatch latch = new CountDownLatch[2];  //初始值
latch.await();  //调用了这个方法的那个线程会被挂起,直到count = 0才继续执行,也可以设置时间,
                //超时count还没变0就会继续执行
latch.countDown();  //count值减1

CyclicBarrier(回环栅栏,让一组线程等待到某个状态再全部同时执行,所有等待线程被释放后,CyclicBarrier可以重用)
比如:若干个线程需要进行写操作,并且想所有线程都达到某个状态才能执行后续任务,此时可以用CyclicBarrier。
使用方法如下:

CyclicBarrier barrier = new CyclicBarrier(parties); //参数是指定多个线程达到某状态
                                                    //如果你想执行完任务后,做其他操作可加个Runnable的参数。
barrier.await();    //挂起当前线程,直到到达某个状态再同时执行,同样可以设置时间,超时直接
                    //执行已经到达这个状态的线程

PS:个人简单理解:调了await()会挂起这个线程,然后+1,直到结果等于parties,再继续执行挂起线程的后续部分。

Semaphore(信号量,控制某资源同时被几个线程访问的类,与锁类似)
使用方法如下:

Semaphore semaphore = new Semaphore(5); //设置多少个线程同时访问,可选参数boolean fair表示
                                        //是否公平,即等待越久越先获得许可
semaphore.acquire();    //获取一个许可
semaphore.acquire(int);    //获取多个许可
semaphore.release() //释放一个许可
semaphore.release(int) //释放多个许可

//acquire获取许可的方式会堵塞,就是没有拿到的话会一直等待,如果想立即得到结果
可调用:tryAcquire()

semaphore.availablePermits()    //获得可用许可数目


20.ThreadLocal(线程本地存储)

作用:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,
减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度同时隔离其他线程
你可以:

用法示例

public class ThreadTest {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i =0;i < 5;i++) {
            ThreadTest test = new ThreadTest();
            new Thread(test.new MyThread(i)).start();
        }
    }

    class MyThread implements Runnable {
        private int index;

        public MyThread(int index) {
            this.index = index;
        }

        @Override
        public void run() {
            System.out.println("线程" + index + "的初始value:" + value.get());
            for (int i = 0; i < 10; i++) {
                value.set(value.get() + i);
            }
            System.out.println("线程" + index + "的累加value:" + value.get());
            try {
                Thread.sleep(1000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果

ThreadLocal的实现原理

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,
value是真正需要存储的Object。

更多详细内容可见:[Java并发包学习七]解密ThreadLocal


细讲与代码实现


1.synchronized同步方法或代码块

在Java中每个对象都拥有一个monitor(互斥锁标记),也称为监视器,当多个线程访问某
个对象时,线程只有获得该对象的锁才能访问。使用synchronized关键字来获取对象上的锁,
可应用在方法级别(粗粒度锁)代码块级别(细粒度锁)

同步方法

比如: public synchronized void save(){}

同步代码块

比如:synchronized(资源对象){ }

类锁

每个类都有一个类锁,用于类的静态方法或者一个类的class对象上的(单例),类的对象实例
可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰
的,但是每个类只有一个类锁。类锁与对象锁是两种不同的锁,控制着不同区域,互不干扰,
统一,线程获得对象锁的同时,也可以获取类锁,同时获得两个锁是允许的。
用法比如:public static synchronized insert() {}; synchronized(类.class)

注意事项


2.Lock(锁)

Synchronized的缺陷

使用Synchronized获取锁的线程释放锁的两种情况

如果是IO等待或其他原因(调sleep方法)被堵塞了,但又没释放锁,只能一直等待;
另外当多个线程读写文件时,读和写会冲突,写和写会冲突,但是读与读不该冲突。
而使用Lock可以解决上述问题,而且Lock还可以知道有没有获得锁,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();
}

ReentrantLock(可重入锁,独占锁):唯一实现Lock接口的类

示例代码

public class Main {
    private ArrayList<Integer> arrayList = new ArrayList<>();
    private Lock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        final Main main = new Main();
        for(int i = 0;i < 2;i++) {
            new Thread(){
                @Override
                public void run() {
                    super.run();
                    main.lock(Thread.currentThread());
                }
            }.start();
        }
    }

    /**lock()*/
    public void lock(Thread thread) {
        lock.lock();
        duplicated(thread);
    }

    /**tryLock()*/
    public void tryLock(Thread thread) {
        if(lock.tryLock()) {
            duplicated(thread);
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
    }

    //相同代码
    public void duplicated(Thread thread) {
        try {
            System.out.println(thread.getName() + "得到了锁");
            for(int i=0;i<5;i++) { arrayList.add(i); }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

运行结果

ReadWriteLock接口

读写操作分开成两个锁,一个资源能够被多个读线程访问,或者被一个写线程访问,
但是不能同时存在读写线程,使用场合:共享资源被大量读取操作,只有少量写操作(修改数据)

ReentrantReadWriteLock(读写锁):

readLock()和writeLock()用来获取读锁和写锁,读锁是共享锁,能同时被多个线程获取;
写入锁是独占锁,只能被一个线程锁获取。


3.锁的相关概念

  • 1.可重入锁:synchronized和ReentrantLock都是可重锁,比如线程执行某个synchronized方法
    在这个方法里会调该类中的另一个synchronized方法,此时不用重复申请锁,可以直接执行该方法。

4.生产者与消费者的几种代码实现

synchronized + wait() + notify()方式

实现核心:定义一个仓库类,对于生产和消耗方法加synchronized锁;
定义两个线程,生产者和消费者,对于满或空的情况进行判断,wait()和notify();

产品类:Product.java

public class Product {
    private int productId = 0;
    public Product() {  }
    public Product(int productId) {
        this.productId = productId;
    }
    public int getProductId() {
        return productId;
    }
    public void setProductId(int productId) {
        this.productId = productId;
    }
    @Override
    public String toString() {
        return "Product{" +
                "productId=" + productId +
                '}';
    }
}

仓库类:WareHouse.java

public class WareHouse {
    private int base = 0;
    private int top = 0;
    private Product[] products = new Product[10];
    public synchronized void produce(Product product) {
        notify();
        while (top == products.length) {
            try {
                System.out.println("仓库已满,暂停生产,等待消费者消费...");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        products[top] = product;
        top ++;
    }
    public synchronized Product consume() {
        Product product = null;
        while (top == base) {
            notify();
            try {
                System.out.println("仓库已空,暂停消费,等待生产者生产...");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        top--;
        product = products[top];
        products[top] = null;
        return product;
    }
}

生产者线程:Producer.java:

public class Producer implements Runnable {
    private String produceName;
    private WareHouse wareHouse;
    public Producer() { }
    public Producer(String produceName, WareHouse wareHouse) {
        this.produceName = produceName;
        this.wareHouse = wareHouse;
    }
    public String getProduceName() {
        return produceName;
    }
    public void setProduceName(String produceName) {
        this.produceName = produceName;
    }
    @Override
    public void run() {
        int i = 0;
        int j = 0;
        while (j < 100) {
            i++;
            j++;
            Product product = new Product(i);
            wareHouse.produce(product);
            System.out.println(getProduceName() + "生产了" + product);
            try {
                Thread.sleep(200l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者线程:Consumer.java

public class Consumer implements Runnable{
    private String consumerName = null;
    private WareHouse wareHouse = null;
    public Consumer() { }
    public Consumer(String consumerName, WareHouse wareHouse) {
        this.consumerName = consumerName;
        this.wareHouse = wareHouse;
    }
    public String getConsumerName() {
        return consumerName;
    }
    public void setConsumerName(String consumerName) {
        this.consumerName = consumerName;
    }
    @Override
    public void run() {
        int j = 0;
        while (j < 100) {
            j++;
            System.out.println(getConsumerName() + "消费了" + wareHouse.consume());
            try {
                Thread.sleep(300l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类:Test.java

public class Test {
    public static void main(String[] args) {
        WareHouse wareHouse = new WareHouse();
        Producer producer = new Producer("生产者",wareHouse);
        Consumer consumer = new Consumer("消费者",wareHouse);
        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(consumer);
        t1.start();
        t2.start();
    }
}

Lock + Condition接口的await()与signal()实现

和上面代码没太大区别,只是改了下仓库类:ConditionWareHouse.java:
这里可以只用一个Condition,把signal改成signalAll()即可。

public class ConditionWareHouse {
    private LinkedList<Product> products = new LinkedList<>();
    private static final int MAX_SIZE = 10; //仓库容量
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();
    public  void produce(Product product) {
        lock.lock();
        try {
            while (products.size() == MAX_SIZE) {
                System.out.println("仓库已满,暂停生产,等待消费者消费...");
                notFull.await();
            }
            products.add(product);
            notEmpty.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public  Product consume() {
        lock.lock();
        Product product = null;
        try {
            while (products.size() == 0) {
                System.out.println("仓库已空,暂停消费,等待生产者生产...");
                notEmpty.await();
            }
            product = products.removeLast();
            notFull.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return product;
    }
}

使用堵塞队列实现

public class ThreadTest {

    private static final int MAX_SIZE = 10; //仓库容量
    private  ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(MAX_SIZE);

    public static void main(String[] args) {
        ThreadTest test = new ThreadTest();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
        new Thread(producer).start();
        new Thread(consumer).start();
    }

    class Producer implements Runnable {
        @Override
        public void run() {
            for(int i =0;i < 100;i++) {
                try {
                    queue.put(i);
                    System.out.println("往仓库中放入元素(" + i + ")剩余容量为:" + (MAX_SIZE - queue.size()));
                    Thread.sleep(50l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Consumer implements Runnable {
        @Override
        public void run() {
            for(int i =0;i < 100;i++) {
                try {
                    queue.take();
                    System.out.println("从仓库中取走元素(" + i + ")剩下元素:" + queue.size());
                    Thread.sleep(100l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5.volatile关键字详解

深入剖析volatile关键字

volatile的原理与实现机制

摘自:《深入理解Java虚拟机》:
"观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键
字时,会多出一个lock前缀指令” lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),
内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面
的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile关键字的使用场景

状态量标记

//状态量标记
volatile boolean flag = false;
while(!flag){
    doSomething();
}
public void setFlag() {
    flag = true;
}
//多线程
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

双重校验锁

class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() { }
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

MusicTime

:静心~大悲咒

<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="http://music.163.com/outchain/player?type=2&id=344719&auto=1&height=66"></iframe>


本文内容部分摘自

上一篇 下一篇

猜你喜欢

热点阅读