您的位置:首页 > 编程语言 > Java开发

JAVA多线程基础 之二 什么是线程安全及解决办法

2019-05-22 08:48 393 查看

什么是线程安全?

保证线程安全需要保证几个基本特性:

  1. 原子性:相关操作不会被其他线程所打扰,一般通过同步机制实现。
  2. 可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓。通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
  3. 有序性:保证线程内的串行语义,避免指令重排等。

 

线程安全解决办法?

内置的锁(synchronized)

Java提供了一种内置的锁(Intrinsic Lock)机制来支持原子性,每一个Java对象都可以用作一个实现同步的锁,称为内置锁,也叫隐式锁。

线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁。

内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁。

synchronize是可重入锁。

内置锁使用synchronized关键字实现,synchronized关键字有两种用法:

1.同步方法

在方法上修饰synchronized 称为同步方法,此时充当锁的对象为调用同步方法的对象。

非静态同步函数使用this锁。

//非静态synchronized修饰方法 使用的是this锁
private synchronized void sale() {
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
        System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");
        ticketCount--;
}

静态同步函数

方法上加上static关键字,使用synchronized关键字修饰或者使用类.class文件。

静态的同步函数使用的锁是该函数所属字节码文件对象

可以用getClass方法获取,也可以用当前类名.class 表示。

//static synchronized == synchronized (DeadLockDemo.class) 锁为当前的字节码文件
    private static synchronized void sale() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
            System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");
        ticketCount--;
    }

2.同步代码块

synchronize(任意全局变量){需要被同步的代码}

和直接使用synchronized修饰需要同步的方法是一样的,但是锁的粒度可以更细,并且充当锁的对象不一定是this,也可以是其它对象,所以使用起来更加灵活。

synchronized (obj) {
    if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
        System.out.println(Thread.currentThread().getName() + "售出第 " + (100 - ticketCount + 1) + "张票");
        ticketCount--;
}

上例中使用Object充当锁对象。

synchronized(this){
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    if (< 3ff7 em>ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次
        System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");
    ticketCount--;
}

上例中使用this锁。同非静态同步函数

注意: 保证多线程同步时必须保证多个线程使用同一个锁 。

3.底层实现

synchronized代码块是由一对monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元。

JVM提供了三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁。

4.锁的升级降级

所谓锁的升级降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状态状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

没有竞争出现的时候,默认会使用偏斜锁。JVM会利用CAS操作(compare and swap),在对象头上的Mark word部分设置线程ID,以表示对象偏向于当前线程,所以不涉及真正的互斥锁。

这样做的假设是基于在很多应用场景中,大部分对象生命周期最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

如果有另外的线程试图锁定某个已经被偏离过的对象,JVM就要撤销(revoke)这个对象的偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,则使用普通的轻量级锁;否则,进一步升级为重量级锁。

当JVM进入安全点(safepoint)的时候,会检查是否有闲置的Monitor,然后试图降级。

偏斜锁、轻量级锁、重量级锁的代码实现并不在核心类库中,而是在JVM的代码中。

关闭偏斜锁:

-XX:-UseBiasedLocking

5.常见问题

不要使用String常量加锁

注意:不要使用String常量加锁,会引起死循环的问题。

public class StringLock {

    public void method(){

        //使用字符串常量加锁 只进入t1

        synchronized ("lock")

        //使用字符串常量加锁 如下则没有问题

        synchronized (new String("lock"))

        {

            try {

                while (true)

                {

                    System.out.println(Thread.currentThread().getId()+"------thread start--------");

                    Thread.sleep(1000);

                    System.out.println(Thread.currentThread().getId()+"------thread end----------");

                }

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

    public static void main(String[] args){

        StringLock stringLock = new StringLock();

        Thread t1 = new Thread(new Runnable() {

            @Override

            public void run() {

                stringLock.method();

            }

        });

        Thread t2 = new Thread(new Runnable() {

            @Override

            public void run() {

                stringLock.method();

            }

        });

        t1.start();

        t2.start();

    }

}

不要修改锁

可以修改其成员变量,但不要修改其引用。修改了引用就会释放锁。

显示锁(Lock)

Lock是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁。

下面我们来分析Lock的几个常见的实现类ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock。

重入锁 ReentrantLock

当一个线程试图获取一个他已经获取的锁的时候,这个获取的动作自动成功。这是一个获取锁粒度的概念,也就是锁的持有以线程为单位,并不基于调用次数。

编码中需要注意必须要明确调用unlock()方法释放,不然就会一直持有该锁。

重入锁可设置公平性(默认为非公平)

ReentrantLock fairLock = new ReentrantLock(true);
fairLock.lock();
try{
    //do something
}finally {
    fairLock.unlock();
}

若使用Synchronized则无法进行公平性选择。它永远都是非公平的。

若要保证公平性则会引入额外开销,会导致一定的吞吐量下降,因此只有程序确实有公平性需要的时候才有必要指定它。

读写锁 ReentrantReadWriteLock

ReadWriteLock(读写锁)是一个接口,提供了readLock和writeLock两种锁的操作,也就是说一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。也就是说读写锁应用的场景是一个资源被大量读取操作,而只有少量的写操作。我们先看其源码:

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();

}

从源码看出,ReadWriteLock借助Lock来实现读写两个锁并存、互斥的机制。每次读取共享数据就需要读取锁,需要修改共享数据就需要写入锁。

读写锁的机制:

1、读-读不互斥,读线程可以并发执行;

2、读-写互斥,有写线程时,读线程会堵塞;

3、写-写互斥,写线程都是互斥的。

举栗子:

public class ReentrantReadWriteLockDemo {


    public static void main(String[] args) {
        final Queue3 q3 = new Queue3();
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        q3.get();
                    }
                }
            }).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread() {
                public void run() {
                    while (true) {
                        q3.put(new Random().nextInt(10000));
                    }
                }

            }.start();
        }
    }
}

class Queue3{
    private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public void get(){
        rwl.readLock().lock();//上读锁,其他线程只能读不能写
        System.out.println(Thread.currentThread().getName() + " be ready to read data!");
        try {
            Thread.sleep((long)(Math.random()*1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "have read data :" + data);
        rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面
    }

    public void put(Object data){
        rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写
        System.out.println(Thread.currentThread().getName() + " be ready to write data!");
        try {
            Thread.sleep((long)(Math.random()*1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.data = data;
        System.out.println(Thread.currentThread().getName() + " have write data: " + data);

        rwl.writeLock().unlock();//释放写锁
    }
}

模拟写一个缓存器

/**
 * 使用ReentrantReadWriteLock模拟一个缓存器
 * Created by zhanghaipeng on 2018/10/24.
 */
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheDemo {
    private Map<String, Object> map = new HashMap<String, Object>();//缓存器
    private ReadWriteLock rwl = new ReentrantReadWriteLock();
    public static void main(String[] args) {

    }
    public Object get(String id){
        Object value = null;
        rwl.readLock().lock();//首先开启读锁,从缓存中去取
        try{
            value = map.get(id);
            if(value == null){  //如果缓存中没有释放读锁,上写锁
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try{
                    if(value == null){
                        value = "aaa";  //此时可以去数据库中查找,这里简单的模拟一下
                    }
                }finally{
                    rwl.writeLock().unlock(); //释放写锁
                }
                rwl.readLock().lock(); //然后再上读锁
            }
        }finally{
            rwl.readLock().unlock(); //最后释放读锁
        }
        return value;
    }

}

Lock与synchronized 的比较

Synchronized是在JVM层面上实现的,无需显示的加解锁,而ReentrantLock和ReentrantReadWriteLock需显示的加解锁,一定要保证锁资源被释放;

Synchronized是针对一个对象的,而ReentrantLock和ReentrantReadWriteLock是代码块层面的锁定;

ReentrantReadWriteLock和ReentrantLock的比较:

ReentrantReadWriteLock是对ReentrantLock的复杂扩展,能适合更加复杂的业务场景,ReentrantReadWriteLock可以实现一个方法中读写分离的锁的机制。而ReentrantLock只是加锁解锁一种机制。

ReentrantReadWriteLock引入了读写和并发机制,可以实现更复杂的锁机制,并发性相对于ReentrantLock和Synchronized更高。

Volatile

可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。

在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。

public class VolatileDemo implements Runnable {

//    private Boolean flag = true;

    //需添加volatile关键字
    private static volatile Boolean flag = true;

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始");
        while (flag)
        {
                System.out.println("线程执行,flag为"+flag);
        }
        System.out.println(Thread.currentThread().getName() + "线程结束");
    }

    public void setRunning(Boolean flag){
        this.flag = flag ;
        System.out.println("setRunning flag -->" + flag);
    }


    public static void main(String[] args){
        VolatileDemo volatileDemo = new VolatileDemo();
        Thread t1 = new Thread(volatileDemo,"t1");
        t1.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        volatileDemo.setRunning(false);
    }
}

上例中的flag需要被volatile修饰后才能保证其线程间可见。

Volatile 保证了线程间共享变量的及时可见性,但不能保证原子性

对异常的处理

根据实际业务选择是否释放锁:

方法一:记录日志并继续

方法二:抛出异常

public class SynchronizedException {

    public synchronized void print() {

        int i = 0 ;

        while (true)

        {

            i++;

            System.out.println(i);

            try {

                Thread.sleep(500);

            if(i==5)

            {

                Integer.parseInt("a");

            }

            } catch (Exception e) {

                e.printStackTrace();

                //方法一:记录日志&contiunue

                System.out.println(Thread.currentThread().getId());

//                continue;

                //方法二:抛出RuntimeException()

                throw new RuntimeException();

            }

        }

    }

    public static void main(String[] args){

        final SynchronizedException synchronizedException = new SynchronizedException();

        Thread t = new Thread(new Runnable() {

            @Override

            public void run() {

                synchronizedException.print();

            }

        }) ;

        t.start();

    }

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: