多线程初级

背景

多线程想必大家都不陌生,主要用来在高并发操作时提升性能的同时也带来了并发的问题,那么本篇博客在于梳理和总结多线程的一些知识,如果不对请多多指教及时改正。

理解并发与并行

  • 并行:只两个或者多个事情在同一时刻发生
  • 并发:两个或者多个事件在同一时间间隔发生

synchronized与volatile

  • 好了,废话不多说直接上硬菜,想必接触过多线程大家都不陌生synchronized和volatile这两个字段.那么这两个字段有什么区别呢?

区别

synchronized可以保证事件的原子性和可见性。
volatile只能保证事件的可见性。

这么说想必大家肯定还是不明白,那么就通过实例加注解来详细描述~

  • 使用synchronized关键字
public class T_09_Synchronized implements Runnable{
    public static void main(String[] args) {
        T_09_Synchronized t_09_synchronized = new T_09_Synchronized();
        for(int i=0;i<5;i++){
            //起五个线程
            new Thread(t_09_synchronized).start();
        }

    }

    private int count = 10;

    //使用synchronized关键字,保证了线程原子性,多个线程并发操作不影响结果
    @Override
    public synchronized void run() {
        count--;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName()+"count="+count);
    }
}

运行结果

Thread-0count=9
Thread-3count=8
Thread-4count=7
Thread-2count=6
Thread-1count=5
  • 使用volatile关键字
  • 案例一
public class T_09_Synchronized implements Runnable{
    public static void main(String[] args) {
        T_09_Synchronized t_09_synchronized = new T_09_Synchronized();
        for(int i=0;i<5;i++){
            //起五个线程
            new Thread(t_09_synchronized).start();
        }

    }

    private volatile int count = 10;

    //使用synchronized关键字,保证了线程原子性,多个线程并发操作不影响结果
    @Override
    public void run() {
        count--;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName()+"count="+count);
    }
}

执行结果

Thread-4count=5
Thread-3count=5
Thread-2count=5
Thread-1count=5
Thread-0count=5
  • 案例二
public class T_10_VolatileDemo implements Runnable {

    //如果不加volatile永远执行不到end
    private volatile  boolean  running = true;

    public static void main(String[] args) throws InterruptedException {
        T_10_VolatileDemo t_10_volatileDemo = new T_10_VolatileDemo();
        new Thread(t_10_volatileDemo).start();
        TimeUnit.SECONDS.sleep(1);
        t_10_volatileDemo.running = false;
    }

    @Override
    public void run() {
        System.out.println("start");
        while (running){

        }
        System.out.println("end");
    }
}
  • 执行结果
start
end

好了,介绍完两者之间的区别,相信读者大致有对这两个关键字有所了解,那么接下来我们就来深入的了解下这两者各自的特性

synchronized

  • 锁的是对象不是代码
  • 锁定方法和非锁定方法可以同时执行
  • synchronized保证数据的可见性和原子性

何为synchronized锁的细化?

  • 在真正需要使用到锁的地方加上synchronized关键字
//锁的细化
public class FineCoarseLock {
    int count = 0;

    void run() {
        //这里是要处理的业务逻辑,这里使用sleep来代替
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //当只有这里是有共享数据,将锁加载这 而不是加在 synchronized void run()上
        synchronized(this){
            count++;
        }
        //这里是要处理的业务逻辑,这里使用sleep来代替
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

何为synchronized锁的粗化?

  • 锁的粗话涉及到一个锁升级机制,那么何为锁升级机制

偏向锁

  • 线程来之后不加锁,记录线程的id。下次再有线程进来的时候,认为这是上次进来的线程,如果还是上次进来的线程就继续运行不考虑加锁。如果是新的线程不是原来的线程就进行一个锁升级;

自旋锁

  • 使用场景(执行时间短,线程少,使用自旋锁)
  • 当有线程将对象锁住之后,自旋锁在外面不断徘徊判断是否能拿到这个锁,当自旋十次还拿不到,执行锁升级。

重量级锁

  • 使用场景(执行时间长,线程多,使用os锁)
  • 经过os进入等待队列里

volatile

  • 作用:保证线程的可用性,禁止指令重排序
  • 可见性的实现:缓存一致性
  • 指令重排序的实现: 添加内存屏障

好了,大概我所知道的synchronized和volatile的区别与各自的有点就总结到这。接下来我们说一说锁的几种机制。

乐观锁(CAS)

  • 什么是乐观锁?

简而言之,乐观锁就好比行数。当你期望的值与新值不相同的时候才进行比较。

  • 使用乐观锁会产生什么问题吗?

比较典型的就是ABA问题

  • 什么是ABA问题?

当多个线程访问时候,当线程A需要修改1为2的时候,此时B线程刚好拿到值把1改成3,C线程又把3改成1,此时A线程就会出现所谓的ABA问题。

  • 如何解决ABA问题?

添加版本号或者时间戳。

  • 下面我们来举个栗子,具体说一下加不同锁机制的执行效率是怎么样的。
/**
 * 比较不同锁的执行效率
 */
public class AtomicIntegerDemo {

    /**
     * CAS锁
     */
    private static AtomicInteger count1 = new AtomicInteger(0);

    /**
     * 加synchronized 相当于悲观锁,效率低
     */
    private static long count2 = 0;

    /**
     * CAS+分段锁 当执行的线程较大的时候 效率高
     */
    private static LongAdder count3 = new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(()->{
                for (int k=0;k<100000;k++){
                    count1.incrementAndGet();
                }
            });
        }

        long start = System.currentTimeMillis();
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        long end = System.currentTimeMillis();
        System.out.println("AtomicInteger的值为:"+count1.get()+",消耗的时间为:"+(end-start));

        System.out.println("==========================================================");

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(()->{
                for (int k=0;k<100000;k++){
                    synchronized (AtomicIntegerDemo.class){
                        count2++;
                    }
                }
            });
        }

        start = System.currentTimeMillis();
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        end = System.currentTimeMillis();
        System.out.println("count2的值为:"+count2+",消耗的时间为:"+(end-start));


        System.out.println("==========================================================");


        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(()->{
                for (int k=0;k<100000;k++){
                    count3.increment();
                }
            });
        }

        start = System.currentTimeMillis();
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        end = System.currentTimeMillis();
        System.out.println("count3的值为:"+count2+",消耗的时间为:"+(end-start));

    }

}

执行结果

AtomicInteger的值为:100000000,消耗的时间为:1725
==========================================================
count2的值为:100000000,消耗的时间为:1111
==========================================================
count3的值为:100000000,消耗的时间为:615
  • 总结
    在使用不同的锁机制执行的效率也不相同,不能说一定采用CAS或者分布式锁的效率高,要看具体的场景和数据量.

好了,那么我们来说说其他的锁。想必大家面试的时候面试官不仅仅只会问你一个synchronized。接下来我们介绍下ReentrantLock怎么使用

ReentrantLock

  • 说明:

也是一个锁的机制,它与synchronized最大的区别就是需要手动加锁解锁。

  • 采用的结构

采用的是CAS的结构

好了,废话不多说我们先上一个案例演示。

public class ReentrantLockDemo {

    private ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    void run() {
        for (int i = 0; i < 1000; i++) {
            lock.lock();
            try {
                count++;
            } catch (Exception e) {
                e.getMessage();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
        Thread[] threads = new Thread[30];
        for (int i = 0; i < threads.length; i++) {
            threads[i]=new Thread(reentrantLockDemo::run);
        }
        for (Thread thread : threads)  thread.start();
        for (Thread thread : threads)  thread.join();
        System.out.println(reentrantLockDemo.count);
    }
}

-总结
当我们在使用ReentrantLock锁的时候,不管结果最后都需要将锁释放。

那么一个小小的思考,ReentrantLock还有其他的特性吗?答案当然是有的。那我们就来探究下究竟还有那些特性呢?

公平锁机制

  • 添加公平锁机制,使得每个线程进入队列中进行等待。参照代码示例
public class ReentrantLockDemo {

    /**如果此处为false说明是非公平锁,那么程序就是谁先抢到资源那么谁就先执行**/
    private ReentrantLock lock = new ReentrantLock(false);

    void run() {
        for (int i = 0; i < 10; i++) {
            lock.lock();
            try {
                //TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"获得锁.");
            } catch (Exception e) {
                e.getMessage();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
        new Thread(reentrantLockDemo::run).start();
        new Thread(reentrantLockDemo::run).start();
    }
}

执行结果

Thread-0获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
  • 如果是为true的话
public class ReentrantLockDemo {

    /**如果此处为true说明是非公平锁,那么程序就是谁先抢到资源那么谁就先执行,只不过相对分配会以先进先出的原则 但也不能保证**/
    private ReentrantLock lock = new ReentrantLock(true);

    void run() {
        for (int i = 0; i < 10; i++) {
            lock.lock();
            try {
                //TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"获得锁.");
            } catch (Exception e) {
                e.getMessage();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
        new Thread(reentrantLockDemo::run).start();
        new Thread(reentrantLockDemo::run).start();
    }
}

执行结果

Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-1获得锁.
  • 综上所述,执行结果有多条不相同的,所以公平锁和非公平锁其实也是不能保证线程的执行先后的,个人觉得这块了解即可。

  • 好的,那么接下来我们开始来说说CountDownLatch

countDownLatch

  • 作用是什么?
  • 这个类使一个线程等待其他线程各自执行完之后再执行
  • 是通过程序计数器来实现的,计数器的初始值是线程的数量。当每个线程开始执行之后,计数器的值就减1,当计算器的值为0的时候,在等待的线程就可以重新开始工作了。

源码:

  • countDownLatch类中只提供了一个构造器:
//参数count为计数值
public CountDownLatch(int count) {  }; 
  • 类中有三个方法最重要
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };

那么下面我们就通过具体的案例来说明讲解改如何使用

public class CountDownLatchDemo {

    private static ReentrantLock lock = new ReentrantLock();
    //定义
    private static CountDownLatch countDownLatch = new CountDownLatch(30);
    private static int count = 0;


    static void run() {
        for (int i = 0; i < 100; i++) {
            lock.lock();
            try {
                count++;
            } catch (Exception e) {
                e.getMessage();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[30];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                run();
                //每次start一个线程 count数量-1
                countDownLatch.countDown();
            });
        }
        for (Thread t : threads) {
            t.start();
        }
        try {
            //使主线程处于等待状态
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

执行结果

3000

CyclicBarrier

在介绍它之前我们必须着重清楚什么是CyclicBarrier,它是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共的屏障点。

  • 直接使用例子说明
public class CycliBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(20, () -> {
            System.out.println("当线程满20个,就放行!");
        });

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

ReadWriteLock(读写锁)

在我们的实际应用场景中,当读数据的时候我们应该时共享锁,为写数据的时候我们应该是互斥锁。那么ReadWriteLock就能很好的解决此类问题。

public class ReadWriteLockDemo {

    //定义读写锁
    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //定义读锁
    static Lock readLock=readWriteLock.readLock();

    //定义写锁
    static Lock writeLock=readWriteLock.writeLock();

    static int value  = 0;


    public static void main(String[] args) {
        for(int i=0 ;i<10;i++){
            new Thread(()->{
                try {
                    readLock.lock();
                    Thread.sleep(1000);
                    System.out.println("定义读锁");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    readLock.unlock();
                }
            }).start();
        }

        System.out.println("========");

        for(int k=0 ;k<10;k++){
            new Thread(()->{
                try {
                    writeLock.lock();
                    Thread.sleep(1000);
                    System.out.println("定义写锁");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }).start();
        }
    }
}

执行结果(读锁一次性全部出来,写锁每隔1S出来一个)

========
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁

LockSupport

LockSuppot是一个线程阻塞的工具类,它的所有方法都是静态方法。可以在线程的任意位置阻塞。
类似于notify/wait 区别在于
notify只能唤醒任意一个线程,但是unpark可以唤醒执行线程

  • 案例
public class LockSupportDemo {
    public static void main(String[] args) {

        Thread thread = new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(i);
                    if(i==6){
                        LockSupport.park();
                    }
                    if(i==8){
                        LockSupport.park();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();

        //只能让第一次的LockSupport.park();生效
        LockSupport.unpark(thread);
    }
}

执行结果 (只能让第一次执行LockSupport.park()生效,后面会阻塞)

Connected to the target VM, address: '127.0.0.1:56743', transport: 'socket'
0
1
2
3
4
5
6
7
8
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值