【java并发】线程锁技术的使用

  线程锁好比传统线程模型中的synchronized技术,但是比sychronized方式更加面向对象,与生活中的锁类似,锁本身也应该是个对象。两个线程执行的代码片段如果要实现同步互斥的效果,它们必须用同一个锁对象。锁是上在代表要操作的资源的类的内部方法中,而不是线程代码中。这一篇博文主要总结一下线程锁技术中Lock锁、ReadWriteLock锁的使用。

1. Lock的简单使用

  有了synchronized的基础,Lock就比较简单了,首先看一个实例:

public class LockTest {

    public static void main(String[] args) {

        new LockTest().init();
    }

    private void init() {
        final Outputer outputer = new Outputer();
        // 线程1打印:duoxiancheng
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    outputer.output("duoxiancheng");
                }

            }
        }).start();
        ;

        // 线程2打印:eson15
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    outputer.output("eson15");
                }

            }
        }).start();
        ;
    }

    // 自定义一个类,保存锁和待执行的任务
    static class Outputer {
        Lock lock = new ReentrantLock(); //定义一个锁,Lock是个接口,需实例化一个具体的Lock
        //字符串打印方法,一个个字符的打印
        public void output(String name) {

            int len = name.length();
            lock.lock();
            try {
                for (int i = 0; i < len; i++) {
                    System.out.print(name.charAt(i));
                }
                System.out.println("");
            } finally {
                lock.unlock(); //try起来的原因是万一一个线程进去了然后挂了或者抛异常了,那么这个锁根本没有释放
            }
        }
}

  这个例子和前面介绍synchronized的例子差不多,区别在于将synchronized改成了lock。从程序中可以看出,使用Lock的时候,需要先new一个Lock对象,然后在线程任务中需要同步的地方上锁,但是一定要记得放锁,所以使用try块去处理了一下,将放锁的动作放在finally块中了。

  这是一个线程任务的情况,如果两个线程任务也不麻烦,还是在这个类中新建一个任务方法,因为Lock是这个类的成员变量,还是可以用这个lock,而且必须用这个lock,因为要实现同步互斥,必须使用同一把锁。

2. 读写锁的妙用

  锁又分为读锁和写锁,读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,这是由jvm自己控制的。这很好理解,读嘛,大家都能读,不会对数据造成修改,只要涉及到写,那就可能出问题。 我们写代码的时候只要在正确的位置上相应的锁即可。读写锁有个接口叫ReadWriteLock,我们可以创建具体的读写锁实例,通过读写锁也可以拿到读锁和写锁。下面看一下读写锁的例子:

2.1 读写锁的基本用法

public class ReadWriteLockTest {

    public static void main(String[] args) {
        final Queue3 q3 = new Queue3(); //封装共享的数据、读写锁和待执行的任务的类

        for (int i = 0; i < 3; i++) {
            new Thread() { // 开启三个线程写数据
                public void run() {
                    while (true) {
                        q3.put(new Random().nextInt(10000));
                    }
                }
            }.start();

            new Thread() { // 开启三个线程读数据
                public void run() {
                    while (true) {
                        q3.get();
                    }
                }
            }.start();
        }
    }
}

class Queue3 {

    private Object data = null; // 共享的数据
    private ReadWriteLock rwl = new ReentrantReadWriteLock();// 定义读写锁

    // 读取数据的任务方法
    public void get() {
        rwl.readLock().lock(); // 上读锁
        try {
            System.out.println(Thread.currentThread().getName()
                    + ":before read: " + data); // 读之前打印数据显示

            Thread.sleep((long) (Math.random() * 1000)); // 睡一会儿~

            System.out.println(Thread.currentThread().getName()
                    + ":after read: " + data); // 读之后打印数据显示
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwl.readLock().unlock();// 释放读锁
        }
    }

    // 写数据的任务方法
    public void put(Object data) {
        rwl.writeLock().lock(); // 上写锁
        try {
            System.out.println(Thread.currentThread().getName()
                    + ":before write: " + this.data); // 读之前打印数据显示

            Thread.sleep((long) (Math.random() * 1000)); // 睡一会儿~
            this.data = data; //写数据

            System.out.println(Thread.currentThread().getName()
                    + ":after write: " + this.data); // 读之后打印数据显示
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwl.writeLock().unlock();// 释放写锁
        }
    }
}

  为了说明读锁和写锁的特点(读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥),我先把上面两个任务方法中上锁和释放锁的四行代码注释掉,来看一下运行结果:

  其实不管是注释掉读锁还是注释掉写锁还是全注释掉,都会出问题,写的时候会有线程去读。那么将读写锁加上后,再看一下运行结果:

  可以看出,有了读写锁,各个线程运行有序,从结果来看,也印证了读锁与读锁不互斥,写锁与读锁、写锁都互斥的特点。

2.2 读写锁用于缓存数据

  现在使用读写锁写一个稍微高级一点的应用demo,即模拟缓存数据。实现的功能如下:现在有5个线程都需要拿数据,一开始是没有数据的,所以最先去拿数据的那个线程发现没数据,它就得去初始化一个数据,然后其他线程拿数据的时候就可以直接拿了。代码如下:

public class ReadWriteLockTest2 {

    public static void main(String[] args) {

        CacheData cache = new CacheData();

        for(int i = 1; i <= 5; i ++) { //开启5个线程
            new Thread(new Runnable() {

                @Override
                public void run() {
                    cache.processCache(); //都去拿数据
                }
            }).start();
        }
    }
}

class CacheData {

    private Object data = null; // 需要缓存的数据
    private boolean cacheValid; //用来标记是否有缓存数据
    private ReadWriteLock rwl = new ReentrantReadWriteLock();// 定义读写锁

    public void processCache() {
        rwl.readLock().lock(); //上读锁

        if(!cacheValid) { //如果没有缓存,那说明是第一次访问,需要给data赋个值
            rwl.readLock().unlock(); //先把读锁释放掉
            rwl.writeLock().lock(); //上写锁
            if(!cacheValid) {
                System.out.println(Thread.currentThread().getName() + ": no cache!");
                data = new Random().nextInt(1000); //赋值
                cacheValid = true; //标记已经有缓存了
                System.out.println(Thread.currentThread().getName() + ": already cached!");
            }
            rwl.readLock().lock(); //再把读锁上上
            rwl.writeLock().unlock(); //把刚刚上的写锁释放掉
        }
        System.out.println(Thread.currentThread().getName() + " get data: " + data);
        rwl.readLock().unlock(); //释放读锁
    }
}

  从代码中可以看出,在processCache方法中对读锁和写锁的交替使用。一开始进来都是读数据的,所以一开始都是上了读锁,但当第一个线程进来发现没有缓存数据的时候,它得写数据,那么此时它得先把读锁给释放掉,换了把写锁,告诉其他线程:”哎哥们,这里面边儿根本没数据啊,我们被坑了,让我先弄个数据来吧,不好意思你们先等会儿~“,等该线程初始化好了数据后,其他线程就可以读了,于是它又把读锁装起来了,把写锁释放了,然后它出去了。这就模拟了拿缓存数据的一个demo,可以看出,在一个方法中,同一个线程可以操作两个锁的。看一下运行结果:

Thread-1: no cache!

Thread-1: already cached!

Thread-1 get data: 893

Thread-0 get data: 893

Thread-2 get data: 893

  这和Hibernate中的那个load(id, Class.class)方法有点类似,先拿到的是代理对象,要使用该对象的时候,如果发现没有,就新产生一个,如果有了就直接拿来用。

2.3 读写锁用于缓存系统

  继续进阶,如果现在要缓存多个数据,即要写一个缓存系统,那该如何做呢?一个缓存系统无非就是一个容器,可以存储很多缓存数据,很自然的想到使用一个Map,专门装缓存数据,然后供多个线程去使用。所以整个设计思路,跟上面缓存单个数据是一样的,不过就是多考虑一些东西而已,看下代码:

public class CacheDemo {

    public static void main(String[] args) {

        Cache cac = new Cache();
        for(int i = 0; i < 3; i ++) { //开启三个线程去缓存中拿key为cache1的数据,
            new Thread(new Runnable() {

                @Override
                public void run() {
                    String value = (String) cac.getData("cache1"); //第一个进入的线程要先写一个数据进去(相当于第一次从数据库中取)
                    System.out.println(Thread.currentThread().getName() + ": " + value);
                }
            }).start();
        }

        for(int i = 0; i < 3; i ++) { //开启三个线程去缓存中拿key为cacahe2的数据
            new Thread(new Runnable() {

                @Override
                public void run() {
                    String value = (String) cac.getData("cache2");//第一个进入的线程要先写一个数据进去(相当于第一次从数据库中取)
                    System.out.println(Thread.currentThread().getName() + ": " + value);
                }
            }).start();
        }
    }
}

class Cache {
    //存储缓存数据的Map,注意HashMap是非线程安全的,也要进行同步操作
    private Map<String, Object> cache = Collections.synchronizedMap(new HashMap<String, Object>());
    private ReadWriteLock rwl = new ReentrantReadWriteLock(); //定义读写锁

    public synchronized Object getData(String key) {
        rwl.readLock().lock(); //上读锁
        Object value = null;
        try {
            value = cache.get(key); //根据key从缓存中拿数据
            if (value == null) { //如果第一次那该key对应的数据,拿不到
                rwl.readLock().unlock(); //释放读锁
                rwl.writeLock().lock(); //换成写锁
                try {
                    if (value == null) { //之所以再去判断,是为了防止几个线程同时进入了上面那个if,然后一个个都来重写赋值一遍
                        System.out.println(Thread.currentThread().getName() + " write cache for " + key);
                        value = "aaa" + System.currentTimeMillis(); // 实际中是去数据库中取,这里只是模拟
                        cache.put(key, value); //放到缓存中
                        System.out.println(Thread.currentThread().getName() + " has already written cache!");
                    }
                } finally {
                    rwl.writeLock().unlock(); //写完了释放写锁
                }
                rwl.readLock().lock(); //换读锁
            }
        } finally {
            rwl.readLock().unlock(); //最后呢释放读锁
        }
        return value; //返回要取的数据
    }
}

  整个代码的结构和上面的一样,理解了缓存单个数据后,这个代码也不难理解。这里只是个demo,实际中可以是跟数据库打交道,第一次从缓存中拿肯定是没有的,那么就要去数据库中查,然后把取到的数据放到缓存中,下次别的线程来就能直接从缓存中取了。看一下运行结果:

Thread-0 write cache for cache1

Thread-0 has already written cache!

Thread-4 write cache for cache2

Thread-0: aaa1464782404722

Thread-4 has already written cache!

Thread-4: aaa1464782404723

Thread-3: aaa1464782404723

Thread-2: aaa1464782404722

Thread-1: aaa1464782404722

Thread-5: aaa1464782404723

  从结果可以看出,线程0首先去缓存中拿key为cache1的值,没拿到,往里面写了一个,然后线程4去缓存中拿key为cache2的值也没拿到,于是也写了一个,在此期间线程0把值拿了出来,后面几个线程也随后陆续的拿出来了。

  读写锁的应用还是很广泛的,而且很好用。关于线程锁的技术就总结这么多吧。

  

  相关阅读:http://blog.csdn.net/column/details/bingfa.html



—–乐于分享,共同进步!

—–更多文章请看:http://blog.csdn.net/eson_15

时间: 2024-10-11 07:58:27

【java并发】线程锁技术的使用的相关文章

Java 并发 线程同步

Java 并发 线程同步 @author ixenos 同步 1.异步线程本身包含了执行时需要的数据和方法,不需要外部提供的资源和方法,在执行时也不关心与其并发执行的其他线程的状态和行为 2.然而,大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取,这将产生同步问题(可见性和同步性的丢失) 比如两个线程同时执行指令account[to] += amount,这不是原子操作,可能被处理如下: a)将account[to]加载到寄存器 b)增加amount c)将结果写回acco

Java 并发 线程的优先级

Java 并发 线程的优先级 @author ixenos 低优先级线程的执行时刻 1.在任意时刻,当有多个线程处于可运行状态时,运行系统总是挑选一个优先级最高的线程执行,只有当线程停止.退出或者由于某些原因不执行的时候,低优先级的线程才可能被执行 2.两个优先级相同的线程同时等待执行时,那么运行系统会以round-robin的方式选择一个线程执行(即轮询调度,以该算法所定的)(Java的优先级策略是抢占式调度!) 3.被选中的线程可因为一下原因退出,而给其他线程执行的机会: 1) 一个更高优先

[并发]线程池技术小白

1  线程池技术介绍 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收.所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁.如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因.比如大家所熟悉的数据库连接池正是遵循这一思想而产生的,本文将介绍的线程池技术同样符合这一思想.

Java 并发 线程的生命周期

Java 并发 线程的生命周期 @author ixenos 线程的生命周期 线程状态: a)     New 新建 b)     Runnable 可运行 c)     Running 运行 (调用getState()时显示为Runnable) d)     Blocked 阻塞 i.          I/O阻塞 (不释放锁) I/O操作完成解除阻塞,进入Runnable状态 ii.          同步阻塞(不释放锁) 运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会

11.Java5的线程锁技术

1 import java.util.concurrent.locks.Lock; 2 import java.util.concurrent.locks.ReentrantLock; 3 4 5 /** 6 * java5的线程锁技术 7 * Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似. 8 * 锁本身也应该是一个对象.两个线程执行的代码片段要实现同步互斥的效果,它们必须 9 * 用在同一个Lock对象.锁是上在代表要操作的资源的类的内部方法中,而不是

Java 并发 线程属性

Java 并发 线程属性 @author ixenos 线程优先级 1.每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程 2.默认情况下,一个线程继承它的父线程的优先级 当在一个运行的线程A里,创建另一个线程B的时候,那么A是父线程,B是子线程.当在一个运行的线程A里,创建线程B,然后又创建了线程C,这时候虽然B比C创建早,可是B并不是C的父线程,而A是B和C的父线程. 3.线程的优先级高度依赖于系统,当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台

Java并发——线程间通信与同步技术

传统的线程间通信与同步技术为Object上的wait().notify().notifyAll()等方法,Java在显示锁上增加了Condition对象,该对象也可以实现线程间通信与同步.本文会介绍有界缓存的概念与实现,在一步步实现有界缓存的过程中引入线程间通信与同步技术的必要性.首先先介绍一个有界缓存的抽象基类,所有具体实现都将继承自这个抽象基类: public abstract class BaseBoundedBuffer<V> { private final V[] buf; priv

Java并发编程与技术内幕:线程池深入理解

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要: 本文主要讲了Java当中的线程池的使用方法.注意事项及其实现源码实现原理,并辅以实例加以说明,对加深Java线程池的理解有很大的帮助. 首先,讲讲什么是线程池?照笔者的简单理解,其实就是一组线程实时处理休眠状态,等待唤醒执行.那么为什么要有线程池这个东西呢?可以从以下几个方面来考虑:其一.减少在创建和销毁线程上所花的时间以及系统资源的开销 .其二.2将当前任务与主线程隔离,能实现和主

Java并发编程与技术内幕:聊聊锁的技术内幕(上)

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 一.基础知识 在Java并发编程里头,锁是一个非常重要的概念.就如同现实生活一样,如果房子上了锁.别人就进不去.Java里头如果一段代码取得了一个锁,其它地方再想去这个锁(或者再执行这个相同的代码)就都得等待锁释放.锁其实分成非常多.比如有互斥锁.读写锁.乐观锁.悲观锁.自旋锁.公平锁.非公平锁等.包括信号量其实都可以认为是一个锁. 1.什么时需要锁呢? 其实非常多的场景,如共享实例变量.共