04-java并发编程(基础)

2022-07-18

04-java并发编程(基础)

java并发编程(基础篇)

一. 线程安全是如何产生的

  1. 多个线程操作同一对象
  2. 没有达成原子性,即操作是各自独立的,不是一个整体
	当多个线程并发运行操作同一数据时,由于线程切换的时机不可控,可能会导致操作该数据时的过程未按照程序设计的执行顺序运行,导致操作出现混乱,严重时可能会导致系统瘫痪.

二. 认识线程安全与Synchronized

2.1 线程安全

  • 概念

    当多个线程访问某一个类,对象或者方法时,这个类,对象或方法都能表现出与单线程一致的行为,那么这个类,对象或方法就是线程安全的.
    
  • 缘由

    线程安全问题都是由全局变量及静态变量引起的

  • 注意事项

    若只有读操作,无写操作,那么一般而言是线程安全的

    若有多个线程同时写,一般都要考虑线程安全问题

2.2 synchronized

  • 普通方法上使用synchronized(对象锁)
当一个方法是用synchronized修饰后,那么该方法变为"同步方法",多个线程不能同时进入方法内容运行.而必须有顺序的一个一个运行.这样就可以避免并发安全问题.
	在方法上使用synchronized,那么同步监视器对象就是该方法所属对象,即:方法中看到的

this

  • 执行方式

    • 首先尝试获得锁
    • 如果获得锁,则执行方法体内容
    • 如果无法获得锁则等待,并且不断的尝试获得锁,一旦锁被释放,则多个线程会同时去尝试获得锁,造成锁竞争问题

    锁竞争问题

    在高并发,线程数量高时会引起CPU占用居高不下,或者直接宕机
    
  • 静态方法使用synchronized(类锁)

    静态方法若使用synchronized修饰后,那么该方法一定具有同步效果.
    
    静态方法的锁对象是当前类的类对象,即Class类
    
    Class类的每一个实例用于表示JVM加载的一个类.
    
    当JVM加载一个类的时候就会实例化一个Class的实例用于表示它.每个类在JVM内部都有且只有一个Class实例.
    
  • 互斥锁(即同步运行)

    使用synchronized锁定多段代码,而锁对象相同时,这些代码片段之间就是互斥的.多个线程不能同时执行这些方法.

    即: 对象锁只针对synchronized修饰的方法生效,同一对象中的所有synchronized方法都会同步执行,而非synchronized方法异步执行

2.3 脏读

由于同步和异步方法的执行个性,如果不从全局上进行并发设计很可能会引起数据的不一致,也就是所谓的脏读

多个线程访问同一资源,在一个线程修改数据的过程中,有另外的线程来读取数据,就会引起脏读的产生

为了避免脏读我们一定要保证数据修改操作的原子性,并且对读取操作也要进行同步控制

  • oracle如何防止脏读

    oracle数据在修改前会将原数据放入undo空间中,undo空间可以放入多个版本的快照。因此如果在修改中有数据读取,会读到原始的数据,不会引起脏读

    但是undo空间是有一定的大小限制的,多次修改如果覆盖了最初的undo数据,则会返回snapshot too old异常

2.4 锁重入

同一个线程得到了一个对象的锁之后,再次请求该对象可以再次获得该对象的锁

同一个对象内的多个synchronized方法可以锁重入

父子类可以锁重入,即在子类中sychronized方法可以调用父类的sychronized方法,不会死锁

2.5 抛出异常释放锁

一个线程在获得锁之后执行操作,发生错误抛出异常,则自动释放锁

  • 可以利用抛出异常,主动释放锁
  • 可以在程序异常时防止资源被死锁,无法释放
  • 异常释放锁可能会导致数据不一致

2.6 sychronized代码块

可以达到更细粒度的控制,当前对象锁,类锁,任意对象锁

如果要实现同步效果,则sychronized锁的对象对于两个线程而言是同一对象,即是同一把钥匙

同类型锁之间互斥,不同类型的锁之间互不干扰

2.7 锁失效

  • 不要在线程中修改对象锁的引用,引用被修改会导致锁失效

从这个可以看出,锁的真正是内存地址,即对象的引用所指向的内存空间,同一内存空间加锁就会有效,即用这块内存区域充当了一个监视器,这个监视器用来监视不同线程的调用。但是不同线程必须在一个监视下,不然就不会有效。

  • 在线程中修改了锁对象的属性,而不修改引用则不会引起锁失效,不会产生线程安全问题

2.8 并发与死锁

线程都保持着自己的锁,但是都等待对方先释放锁时就出现了互相"僵持"的情况,导致程序不会再继续向下运行.

2.9 线程之间的通讯

  • Object类中的wait/notify方法可以实现线程间通讯
  • wait/notify必须和synchronized一同使用
  • wait释放锁,notify不释放锁

wait()是Object类的方法,只能用在sychronized方法或同步块中
如未,则抛出IllegalMonitorStateException异常
wait()释放对象锁,使当前线程进入等待池,直到被notify()或者
notifyAll方法唤醒

锁对象中有其他线程使用了notify()方法,则等待池中的随机一个线程被唤醒
进入锁池中。

使用notifyAll()方法,则等待池中的全部线程被唤醒,
进入锁池(竞争锁的池)中。会产生锁竞争问题

notify只会通知一个wait中的线程,并把锁给他,不会产生锁竞争问题,但是该线程处理完毕之后必须再次notify或notifyAll,完成类似链式的操作

2.10 线程安全的阻塞队列

package base;

import java.util.ArrayList;
import java.util.List;

public class MQueue {
    private List<String> list = new ArrayList<>();
    private int maxSize;
    private final Object lock = new Object();

    public MQueue(int maxSize){
        this.maxSize = maxSize;
    }


    /**
     * 线程安全的插入数据
     * @param element
     */
    public void put(String element){
        synchronized (this.lock){           // 这里实现了取和存的线程安全
            if (list.size() == maxSize){
                System.out.println("线程"+Thread.currentThread().getName()+"当前队列已满put等待...");
                try {
                    lock.wait();        //释放对象锁, 并且使当前线程进入等待池,等待被唤醒
                                        // 这里实现了满队列不能存元素
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.list.add(element);
            System.out.println("线程"+Thread.currentThread().getName()+"向队列中加入元素:"+element);
            lock.notifyAll();       //通知可以取数据,这里的作用是,如果取的时候集合为空,那么该线程将进入wait状态,
                                    // 一旦存入一个元素,就通知可以取了,取消线程的wait状态,使其进入锁池竞争
        }
    }

    public String take(){
        synchronized (this.lock){
             if (list.size() == 0){
                 System.out.println("线程"+Thread.currentThread().getName()+"当前队列为空take等待...");
                 try {
                     lock.wait();        //释放对象锁, 并且使当前线程进入等待池,等待被唤醒
                                        //这里实现了空队列不能取元素
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
             String element = this.list.get(0);
             list.remove(0);
             System.out.println("线程"+Thread.currentThread().getName()+"向队列中取出元素:"+element);
             this.lock.notifyAll();         //通知可以放元素
             return element;
        }
    }
}

synchronized负责存和取的线程安全

wait,notify则负责空和满的不出错,完美!!


标题:04-java并发编程(基础)
作者:mahaonan
地址:https://mahaonan.fun/articles/2022/07/18/1658147072890.html