第2章 线程安全与共享资源竞争

第二章 线程安全与共享资源竞争

2.1 synchronized同步介绍

防止出现资源并发冲突的解决思路如下:当共享资源被任务使用时,要对资源提前加锁。所有任务都采用抢占模式,即某个任务会抢先对共享资源加上第一把锁。如果这是一个排他锁,其他任务在资源被解锁前就无法访问了。如果是共享锁,当浏览某些数据时,其他任务也可以同时浏览,但是不允许修改。synchronized关键字的底层,相当于一个排他锁。

2.2 synchronized同步方法

多个线程同时访问同一个对象的某个方法时,如果该方法中存在对共享资源的操作,则可能引发线程安全问题。典型的共享资源有对象的成员变量、磁盘文件、静态变量等。

2.2.1 同步方法调用流程

synchronized同步方法,并不是真的给方法加了锁,它的本质是使用了当前对象的监视器锁,其调用流程如下:

(有个类叫Lock 他有个方法叫timeout() )

(1)线程A调用clock对象的timeout()方法,进入方法体前,先试图获取当前clock对象的监视器锁。

(2)如果clock对象的监视器锁没有被占用,则线程A会获取clock对象的监视器锁,然后进入timeout()方法;否则自旋等待获得锁(一般为抢占式,无须排队)。

(3)其他线程调用timeout()方法时,执行顺序相同。

2.2.2 同步方法之间的互斥

因为执行synchronized方法前必须要获得对象的监视器,同一个对象的多个synchronized方法共享同一个对象监视器,因此我们可以简单总结为,同一个对象的synchronized方法之间是互斥的。即线程A调用time()方法,会阻碍线程B调用timeout()方法。

2.2.3 同步方法与非同步方法

同一对象的同步方法与非同步方法不会产生影响。

2.3 synchronized同步静态方法
2.3.1 单例高并发问题

单例(Singleton)是非常有名的设计模式,就是在程序运行期间,确保只能存在一个对象。其特点如下:

• 构造函数私有化(不允许类外new对象)。

• 单例对象使用static存储。

• 调用单例对象时使用静态方法。

有多个线程同时访问单例模式的新建对象的静态方法的时候,很可能多个线程同时进入到了新建对象那边,那么就会产生多个对象,我们可以在新建对象的时候加一个同步锁就会防止这个问题。

2.3.2 类锁与对象锁

synchronized同步成员方法,本质使用的是当前对象的监视器锁。而synchronized同步静态方法,则使用的是当前Class的监视器锁。 java.lang.Object是所有对象的根,在Object的每个实例中都内置了对象监视器锁

2.3.3 静态同步方法之间互斥

静态同步方法之间是互斥的。

2.3.4 静态同步方法与静态非同步方法

静态同步方法和静态非同步方法之间互不干扰

2.4 synchronized同步代码块

synchronized同步方法,就是线程在调用方法前获取对象监视器锁,方法执行完毕后就释放对象锁。 方法同步的关键是为了保护共享资源,如果synchronized方法中没有使用共享资源,就无须使用synchronized同步这个方法。 在同步方法中,使用共享资源的只是部分代码。为了提高并发性能,一般没必要在整个方法的运行期都持有监视器锁。 使用同步代码块模式,可以在方法中真正需要使用共享资源时再获取监视器锁,共享资源访问结束马上释放锁,这样就节省了对象监视器锁的占用时间,可以有效提高并发性。

2.4.1 锁当前对象

synchronized (this) {}就是获取当前对象的监视器锁。这样可以使对象监视器的使用时间更短,并发性能更高了。

2.4.2 锁其他对象

监视器锁内置与Object的底层,所有的对象都源于Object,因此所有的对象都有监视器锁。使用其他Object对象的监视器锁,会比使用自身对象的监视器锁更加灵活。

synchronized(user){}

2.4.3 锁Class

不仅每个对象都有监视器锁,每个数据类型的class也内置了监视器锁。

synchronized(User.class){}。

synchronized(Object.class){}是类锁不是对象锁,如果项目中有其他的地方也用到了此类,就会造成并发冲突,因此,类锁尽量不要轻易使用。

合理使用类锁的基本原则,尽量使用当前类的监视器锁。

2.5 项目案例:火车售票
2.5.1 共享任务模式

一个Runnable就相当于一个任务

/**
 * 售票任务
 */
public class TicketTask implements Runnable{

    private Integer ticket = 30;

    @Override
    public void run() {
        //这里的while保证了三个线程启动之后只要ticket>0就会一直运行,知道ticket<0结束。
        //三个线程在while循环的时候会一直抢占式的竞争当前对象锁
        while (ticket > 0 ){
            //因为ticket的多线程共享资源,所以要用同步代码块进行保护
            synchronized (this){
                if (ticket > 0 ) {
                    System.out.println("窗口" + Thread.currentThread().getId() + "售出:" + ticket);
                    ticket--;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}
/**
 * 窗口类
 */
public class Curtain extends Thread{

    private String cno;

    public String getCno() {
        return cno;
    }

    /**
     *     public Thread(Runnable target) {
     *         init(null, target, "Thread-" + nextThreadNum(), 0);
     *     }
     */
    public Curtain(Runnable target,  String cno) {
        super(target);
        this.cno = cno;
    }
}
public class main {

    public static void main(String[] args) {
        TicketTask ticketTask = new TicketTask();
        new Curtain(ticketTask,"c01").start();
        new Curtain(ticketTask,"c02").start();
        new Curtain(ticketTask,"c02").start();
    }
}

2.5.2 多任务模式
/**
 * 共享任务模式是锁当前对象,而多任务模式是锁其他对象,
 * (一般情况下多任务模式比共享模式执行的快) 可以这样理解一个任务相当于一只手,一个线程相当于一个人
 * 三个任务三只手,一起执行,去获取锁,肯定比三个人一只手去获取锁执行的快
 */
public class TicketTask implements Runnable{

    private static Integer ticket = 30;

    private static Object obj = new Object();

    private String eno;

    public String getEno() {
        return eno;
    }

    public void saleTicket() {

        if (ticket >0){
            System.out.println("窗口:" +Thread.currentThread().getId()+"售出"+ticket);
            ticket--;
        }
    }

    @Override
    public void run() {
        while (ticket > 0){
            synchronized (obj){
                saleTicket();
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}]()

public class main {

    public static void main(String[] args) {
        new Thread(new TicketTask(),"co1").start();
        new Thread(new TicketTask(),"co2").start();
        new Thread(new TicketTask(),"co3").start();
    }

}
2.6 项目案例
2.7 项目案例
2.8 JDK常见类的线程安全性
2.8.1 集合ArrayList 和Vector

Vector底层是静态数组结构

同一个对象的同步方法共享同一把锁,Vector的很多方法都用到了同步方法,所以在并发环境中用vector集合会严重影响并发性能,JDK为了优化vector的性能新增了ArrayList类

Arraylist没有使用同步访问机制,所以当ArrayList中添加数据的时候,另一个线程访问迭代访问集合中的元素的时候会抛出java.util.ConcurrentModificationException异常。

public class mainss {
    public static void main(String[] args) {
        List<Integer> strings = new ArrayList<>();
        Integer i= 1;
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    strings.add(i);
                }

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    strings.forEach(
                            System.out::println
                    );
                }
            }
        }).start();
    }
}
2.8.2 StringBuffer与StringBuilder

当需要拼接字符串的时候为了避免字符串的常量碎片,通常使用StringBuffer与StringBuilder。StringBuffer有同步机制,StringBuilder没有同步机制。

2.8.3 HashMap与concurrentHashMap

hashMap是一种数组加链表的数据结构,hashMap在put的时候会新建一个初始化长度固定的数组,数组里面储存的时候Node<K,V>,当hash值相同的时候,则通过链表的形式来继续进行储存,当链表的长度大于8的时候,就会把链表的形式装换成红黑树。

HashMap没有同步安全策略,concurrentHashMap则使用了大量的同步代码块,用于增加线程的安全性,并且为了线程安全并且并发性能高,他锁定的范围是很小的节点对象,而不是整个数组或者链表。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值