Java基础巩固 -- 线程同步

Java基础巩固 – 线程同步

1.线程安全

1.1发生原因

在一些情况下,可能会存在多个线程操作一个或多个公共资源。基本步骤是从JVM内存中读取数据,并进行修改,修改完成后重新写回内存。但如果有多个线程存在时,由于线程之间运行的关系,可能导致某一线程处理完的数据被其他线程更改产生脏数据,因此需要通过线程同步的方法解决此类问题。

1.2 解决方案

为了防止不同线程对同一数据的修改使数据编程脏数据,引入线程同步来解决此问题。通过线程同步的方式,让线程按照预先设定好的顺序获取数据防止对数据的错误修改。基于“队列与锁”的思想,JDK中封装了一些用于实现线程同步的类和关键字。我们主要介绍synchronized 和 lock 两种。


2.线程同步

2.1 synchronized

2.1.1 实现原理

在Java中每个对象都会有唯一的一把锁,同时也是实现synchronized的基础。其实现方式有如下三种

  • 实例同步方法:给当前实例对象加锁,执行同步代码前必须获得当前实例的锁。
  • 静态同步方法:给当前类的class对象加锁,执行同步代码前必须获得当前类对象的锁。
  • 同步代码块:给括号中的对象加锁,执行同步代码块必须获得给定对象的锁。
    在实际线程运行过程中,只有一个线程能抢到目标对象的锁,此时其他线程都无法访问该对象的所有synchronized。

2.1.2 实现过程

2.1.2.1 synchronized关键字修饰

在没有实现线程同步时:结果如下

public class Test implements Runnable{

    static int count = 0;
    public void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Test(),"A").start();
        new Thread(new Test(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }

}

14184

Process finished with exit code 0

当使用synchronized关键字

public class Test implements Runnable{

    static int count = 0;
    //synchronized实现线程同步
    public synchronized void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Test(),"A").start();
        new Thread(new Test(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}
20000

Process finished with exit code 0
2.1.2.2 synchronized同步代码块
public class Test implements Runnable{

    static int count = 0;
    //synchronized实现线程同步
    public void increase(){
        synchronized (this) {
            count++;
        }
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Test(),"A").start();
        new Thread(new Test(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

2.2 使用特殊域变量(volatile)实现线程同步

2.2.1 实现原理

volatile关键字为域变量的访问提供了一种免锁的操作机制,使用该关键字修饰的区域相当于告知JVM该域可能会被其他线程更新。因此在使用该域时将会重新计算,而不是调用寄存器中现存的值。最后volatile不提供任何原子操作,也不能修饰final类型变量。

2.2.2 实现过程

public class Test {
    public volatile int count = 0;

    public void increase(){
        //synchronized (this) {
        //    count++;
        //}
        //此时不需要使用synchronized关键字
        count ++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Test(),"A").start();
        new Thread(new Test(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

2.3 重入锁实现线程同步

2.3.1 实现原理

Java在5.0版本中加入了java.util.concurrent包(JUC)支持同步,其中的ReentrantLock类是可重入、互斥并且实现Lock接口的锁。它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。该对象有如下用法:

  • ReentrantLock():创建一个ReentrantLock实例
  • lock():获得锁
  • unlock():释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

2.3.2 实现过程

public class Test {
    public volatile int count = 0;
    Lock lock = new ReentrantLock();

    public void increase(){
        lock.lock();
        try{
            count ++;
        }
        finally{
            lock.unlock();
        }
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Test(),"A").start();
        new Thread(new Test(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

2.4 使用局部变量

2.4.1实现原理

使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。ThreadLocal常用的方法如下:

  • ThreadLocal():创建一个线程本地变量
  • get():返回该线程局部变量的当前线程副本中的值
  • initialValue():返回该线程局部变量的当前线程的初始值
  • set(T value):将该线程局部变量的当前线程副本中的值设为value

2.4.2 实现过程

public class Test {
    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue(){
            return 10;
        }
    };

    public void plus() {
        count.set(count.get() += 1);
    }
}

ThreadLocal与同步机制分别采用空间换时间,时间换空间的方式

2.5 使用阻塞队列

2.5.1 实现原理

通过创建LinkedBlockingQueue阻塞队列可以实现线程同步的方式,其常见方法如下:

  • LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
  • put(E e) : 在队尾添加一个元素,如果队列满则阻塞
  • size() : 返回队列中的元素个数
  • take() : 移除并返回队头元素,如果队列空则阻塞

2.5.2 实现过程

通过阻塞队列实现生产者-消费者线程模型

package com.xhj.thread;

import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingSynchronizedThread {
    /**
     * 定义一个阻塞队列用来存储生产出来的商品
     */
    private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
    /**
     * 定义生产商品个数
     */
    private static final int size = 10;
    /**
     * 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程
     */
    private int flag = 0;

    private class LinkBlockThread implements Runnable {
        @Override
        public void run() {
            int new_flag = flag++;
            System.out.println("启动线程 " + new_flag);
            if (new_flag == 0) {
                for (int i = 0; i < size; i++) {
                    int b = new Random().nextInt(255);
                    System.out.println("生产商品:" + b + "号");
                    try {
                        queue.put(b);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("仓库中还有商品:" + queue.size() + "个");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            } else {
                for (int i = 0; i < size / 2; i++) {
                    try {
                        int n = queue.take();
                        System.out.println("消费者买去了" + n + "号商品");
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("仓库中还有商品:" + queue.size() + "个");
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        BlockingSynchronizedThread bst = new BlockingSynchronizedThread();
        LinkBlockThread lbt = bst.new LinkBlockThread();
        Thread thread1 = new Thread(lbt);
        Thread thread2 = new Thread(lbt);
        thread1.start();
        thread2.start();

    }

}

2.6 使用原子变量实现同步

2.6.1 原子操作

原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即这几种行为要么同时完成,要么都不完成。在JUC包中提供了创建了原子类型变量的工具类。其中AtomicInteger 表可以用原子方式更新int的值,使用方法如下:

  • AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
  • addAddGet(int dalta) : 以原子方式将给定值与当前值相加
  • get() : 获取当前值

2.6.2 实现过程

public class Test {
    AtomicInteger count = new AtomicInteger(10);

    public AtomicInteger getCount () {
        return count;
    }

}
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值