并发4-线程安全

1.线程安全问题:

在多线程中,有可能出现多个线程同时使用同一个资源的情况,这个资源可以是变量,数据表,txt文件等。这个资源称作"临界资源"

举个例子:取钱这个线程分为两个步骤:
1.读取金额
2.取款
3.更新金额

有个典型的线程安全的例子,倘若A,B两人使用同一个账户(1000元)取款,A执行

1.读取金额    2.取款,取出300元,并未更新金额。
此时,
B读取金额,显示为1000(应该为700),B取出1000元。
最后,A执行3.更新金额,B执行3.更新金额。

如前面所说,多个线程访问临界变量,就会产生线程安全问题。不过,当多个线程执行同一个方法时,方法内部的变量不是临界资源,
因为每个线程都有自己独立的内存区域(PC,方法栈,线程栈)

2.解决线程安全问题:

基本所有的并发方案,都采用“序列化访问资源”,也就是在同一时间,只有一个线程能访问临界资源,也称作同步互斥访问

方案1:synchronized

在Java中,每个对象都有一个锁标记,称为monitor(监视器),多个线程访问这个对象的临界资源时,只有获取了该对象的锁才能访问

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

public class ThreadLock2{

    public static void main(String[] agrs) {

        final InsertData insertData = new InsertData();
        new Thread("线程1"){
            @Override
            public void run() {                insertData.insert(Thread.currentThread());
            }

        }.start();

        new Thread("线程2"){
            @Override
            public void run() {
            insertData.insert(Thread.currentThread());
            }
        }.start();
    }
}
class InsertData extends Thread {

    private List<Integer> list = new ArrayList<>();
    //在这其实是锁住了InsertData对象的monitor锁
    public synchronized void insert(Thread thread) {
        for (int i = 0; i < 5; i++) {
            System.out.println(thread.getName() + "插入: " + i);
            list.add(i);
        }
    }
}
输出结果:

线程1插入: 0

线程1插入: 1

线程1插入: 2

线程1插入: 3

线程1插入: 4

线程2插入: 0

线程2插入: 1

线程2插入: 2

线程2插入: 3

线程2插入: 4

试着把  public synchronized void insert(Thread thread)  中的 synchronized去掉,再看看结果

此时,线程1和线程2可能会交叉运行,因为insert这个方法不再是临界资源了(允许多个线程同时进入)

insert方法还可以改成以下两种方式:


class InsertData extends Thread {

    private List<Integer> list = new ArrayList<>();
    public void insert(Thread thread) {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(thread.getName() + "插入: " + i);
                list.add(i);
            }
        }
    }
}

class InsertData extends Thread {

    private List<Integer> list = new ArrayList<>();
    private Object object = new Object();

    public synchronized void insert(Thread thread) {
        synchronized (object) {
            for (int i = 0; i < 5; i++) {
                System.out.println(thread.getName() + "插入: " + i);
                list.add(i);
            }
        }
    }
}
说明:

    1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,
    因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

    2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,
    访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是
    可以访问这个方法的,

    3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使
    object1和object2是同一类型的对象,也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

synchronized的另外一种使用方式:

public class MyThread{

    private Object lock = new Object();
    private ArrayList<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
        MyThread mh = new MyThread();
        ThreadTest tt = mh.new ThreadTest();
        ThreadTest tt2 = mh.new ThreadTest();
        tt.start();
        tt2.start();
    }

    class ThreadTest extends Thread{
        @Override
        public void run() {
            synchronized (lock) {
                for(int i=0;i<5;i++){
                    list.add(i)
                System.out.println(Thread.currentThread().getName()
                            + "insert:" +i);
                }
            }
        }
    }
}
输出结果:

Thread-0insert:0

Thread-0insert:1

Thread-0insert:2

Thread-0insert:3

Thread-0insert:4

Thread-1insert:0

Thread-1insert:1

Thread-1insert:2

Thread-1insert:3

Thread-1insert:4

注意事项:

static方法,使用的是类锁(多个对象使用同一个类的monitor)

和非static方法,使用的是对象锁(每个对象维护一个monitor)

synchronized的缺陷:

1.使用synchronized包住的代码块,只可能有两种状态:顺利执行完毕释放锁,执行发生异常释放锁,不会由于异常导致出现死锁现象

2.如果synchronized包住的代码块中有sleep等操作,比如I/O阻塞,但是其他线程还是需要等待,这样程序的效率就比较低了

3.等待synchronized释放锁的线程会一直等待下去(死心塌地,不到黄河心不死)
时间: 2025-02-01 15:48:17

并发4-线程安全的相关文章

面试题_1_to_16_多线程、并发及线程的基础问题

多线程.并发及线程的基础问题 1)Java 中能创建 volatile 数组吗?能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组.我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了. 2)volatile 能使得一个非原子操作变成原子操作吗?一个典型的例子是在类中有一个 long 类型的成员变量.如果你知道该成员变量会被多个线程访问,如计数器

你真的了解:IIS连接数、IIS并发连接数、IIS最大并发工作线程数、应用程序池的队列长度、应用程序池的最大工作进程数 吗?

IIS连接数 一般购买过虚拟主机的朋友都熟悉购买时,会限制IIS连接数,这边先从普通不懂代码用户角度理解IIS连接数 顾名思义即为IIS服务器可以同时容纳客户请求的最高连接数,准确的说应该叫“IIS限制连接数” 这边客户请求的连接内容包括: 1.网站html请求,html中的图片资源,html中的脚本资源,其他需要连接下载的资源等等,任何一个资源的请求即一次连接(虽然有的资源请求连接响应很快) 2.如果网页采用框架(框架内部嵌套网页请求),那么一个框架即一次连接 3.如果网页弹出窗口(窗口内部嵌

IIS连接数、IIS并发连接数、IIS最大并发工作线程数、应用程序池的队列长度、应用程序池的

IIS连接数 一般购买过虚拟主机的朋友都熟悉购买时,会限制IIS连接数,这边先从普通不懂代码用户角度理解IIS连接数 顾名思义即为IIS服务器可以同时容纳客户请求的最高连接数,准确的说应该叫"IIS限制连接数" 这边客户请求的连接内容包括: 1.网站html请求,html中的图片资源,html中的脚本资源,其他需要连接下载的资源等等,任何一个资源的请求即一次连接(虽然有的资源请求连接响应很快) 2.如果网页采用框架(框架内部嵌套网页请求),那么一个框架即一次连接 3.如果网页弹出窗口(

java之并发编程线程池的学习

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类. corePoolSize在很多地方被翻译成核心池大小,其实我的理解这个就是线程池的大小.举个简单的例子: 假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务. 因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做:

java并发:线程池、饱和策略、定制、扩展

一.序言 当我们需要使用线程的时候,我们可以随时新建一个线程,这样实现起来非常简便,但在某些场景下存在缺陷:如果需要同时执行多个任务(即并发的线程数量很多),频繁地创建线程会降低系统的效率,因为创建和销毁线程均需要一定的时间.线程池可以使线程得到复用,所谓线程复用就是线程在执行完一个任务后并不被销毁,该线程可以继续执行其他的任务. 二.Executors提供的线程池 Executors是线程的工厂类,也可以说是一个线程池工具类,Executors提供的线程都是通过参数设置来实现不同的线程池机制.

java并发编程线程安全

编写线程安全的代码实质就是管理对状态的访问,而且通常是共享的.可变的状态,对象的状态就是数据,存储在状态变量中,比如实例域,或者静态域,同时还包含了其它附属的域,例如hashmap的状态一部分存储到对象本身中,但同时也存储到很多mqp.entry中对象中,一个对象的状态还包含了任何会对他外部可见行为产生影响的数据. 所谓共享是指一个对象可以被多个线程访问, 所谓可变:是指变量的值在其生命周期内可以改变, 真正目的:在不可控制的并发访问中保护数据 线程安全必要条件: 1:对象是否被两个或以上的线程

python 并发和线程

并发和线程 基本概念 - 并行.并发 并行, parallel 互不干扰的在同一时刻做多件事; 如,同一时刻,同时有多辆车在多条车道上跑,即同时发生的概念. 并发, concurrency 同时做某些事,但是强调同一时段做多件事. 如,同一路口,发生了车辆要同时通过路面的事件. 队列, 缓冲区 类似排队,是一种天然解决并发的办法.排队区域就是缓冲区. 解决并发: [ "食堂打饭模型", 中午12点,大家都涌向食堂,就是并发.人很多就是高并发.] 1.队列, 缓冲区: 队列: 即排队.

17.并发编程--线程池

并发编程线程池 合理利用线程池能够带来三个好处. 第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗. 第二:提高响应速度.当任务到达时,任务可以不需要的等到线程创建就能立即执行. 第三:提高线程的可管理性.线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控.但是要做到合理的利用线程池,必须对其原理了如指掌. 1. Executor 框架简介 在 Java 5 之后,并发编程引入了一堆新的启动.调度和管理 线

Java并发编程——线程池的使用

在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPoolExecutor类中的方法讲起,

Ruby 的并发, 进程, 线程, GIL, EventMachine, Celluloid

关于并发与并行, 前不久刚好真实发生. 同事一行人去 Family Mart 买午餐, 拿回来公司只有一个微波炉加热, 在 Family Mart 有两个微波炉可以加热. 也就是说, 我们一行人一起去买午餐这是一个并发的程序, 然后在 Family Mart 可以并行加热, 但是, 如果拿回公司的话, 因为只有一个微波炉(单核), 所以是只能一个接一个顺序执行. 串行执行 给一个 range, 转成 array 以后获取某个特定数字的 index 1 2 3 4 5 6 7 range = 0.