java并发编程

回顾线程基本内容

程序: 静态代码 安装在硬盘上的

进程: 运行中的程序 是操作系统分配内存空间的单位

线程: 线程是进程中的一个最小执行单位, 是cpu调度单位 线程依赖于进程

创建线程

1.线程类 继承 Thread

  1. 类 实现 Runnable接口 重写 run( )

    创建Thread类的对象,为其分配任务

  2. 类 实现 Callable接口 重写 call( ) 有返回值 可以抛出异常

​ 创建Thread类的对象,为其分配任务

常用方法

run() calll() start()

设置名字

设置优先级

线程状态 start() 就绪状态

​ join() sleep() 阻塞状态

​ yield() 线程让步 运行状态 主动让出 —>就绪状态

多线程

程序中如果同时有多个任务执行,需要多线程

多线程可以提高程序运行效率

提高cpu利用率

不足:

对cpu 内存的要求增加了

多线程同时访问同一个共享资源

线程安全问题:

​ 多线程 且 多个线程访问同一个共享数据.

解决办法 排队+锁

Synchronized 关键字 修饰方法 修饰一个代码块

​ 是隐式锁 自动加锁 自动释放锁

ReentrantLock

只能对某段代码加锁 是显示锁 手动添加 手动释放

守护线程

死锁

线程通信

wait() notify()

sleep() 休眠指定的时间 时间到了后,会进入到就绪状态 不会释放锁

wait() 让线程等待 必须需要通过notify()唤醒 等待时会释放锁

生产者 消费者问题

并发编程

并发编程是什么?

并发: 是在某一个时间段内 依次做好几件事情 一个一个的做 多个线程访问同一个资源

​ 单核cpu不会出现问题

​ 硬件cpu的功能逐渐强大, 已经发展到多内核( 4 8 12) 可以同时执行多个线程 这样就有可能多个线程同时执行,同时访问同一个共享数据 . 秒杀 抢购…

​ 并发编程就是要让在多核,多线程情况下,也只能一次只有一个线程对共享数据进行访问.

​ 我们线程已经可以通过加锁的方式实现, 但是加锁并不是唯一的解决方案,还有其他方式来实现.

​ 本篇章我们要学习 并发问题如何产生,如何解决, 学习新的方式解决, 以及加锁内部的原理问题.

​ 多核cpu可以做到真正的并行执行,但是我们在某种场景 就是要让程序并发执行.

并行: 在一个时间节点,可以同时做多件事

多线程优点

提高程序的性能,可以同时执行多个任务.

榨取硬件的剩余价值

多线程不足

​ 安全性(访问共享变量), 性能(cpu要切换线程)

Java 内存模型(JMM)

注意 是java内存模型 不是 JVM模型

java多线程在工作时,现将主内存中的数据 读到线程工作内存(缓存),

然后在工作内存中,对数据进行操作,操作完成后,再将数据写回到主内存.

产生一个 可见性问题?

​ B线程中看不到A线程中操作过的数据.

操作系统可能会对指令的执行先后顺序进行重新排列执行.

​ 会带来有序性问题.

​ int a = 10;

​ int b = 5;

​ int c = a+b;

线程切换带来原子性(不可拆分)问题

i=1; i=1

i++ i=i+1 i++ 2

2

++ 分为先计算 后赋值 ++操作不是原子操作 cpu在执行性可能会分成两步来执行

并发编程核心问题

线程本地缓存 会导致可见性问题

编译优化重排指令 带来有序性问题.

线程切换执行 会导致原子性问题

解决问题

volatile关键字

被volatile修饰后的共享变量, 在一个线程中操作后, 可以保证在另一个线程中立即可见,

被volatile修饰后的共享变量,禁止优化重新排序

volatile 不能保证对变量操作的原子性

i++ 不是原子操作 怎么解决 加锁

public class ThreadDemo implements  Runnable{

    /*
       volatile 修饰的变量,在一个线程中被修改后,对其它线程立即可见
                                            禁止cpu对指令重排序
     */
    private volatile   boolean flag = false;//共享数据

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.flag = true;//让一个线程修改共享变量值
        System.out.println(this.flag);
    }

    public boolean getFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}



public class TestVolatile {

    public static void main(String[] args) {

        ThreadDemo td = new ThreadDemo();
        Thread t = new Thread(td);//创建线程
        t.start();

        //main线程中也需要使用flag变量
        while(true){
            if(td.getFlag()){
                System.out.println("main---------------");
                break;
            }
        }
    }
}

如何保证原子性

加锁

Lock ReentrantLock

synchronized 也能够保证可见性和有序性。

原子类

java.util.concurrent 包

AtomicInteger 通过 volatile + CAS 实现原子操作的

getAndIncrement();  代替 i++  是安全的
public class ThreadDemo implements Runnable{

     //private  int num = 0;//共享变量
    // private volatile int num = 0;
      private AtomicInteger num = new AtomicInteger(0);
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":::"+getNum());
    }

    public int getNum() {
       // return num++;
        return num.getAndIncrement();
    }


}



public class Test {

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();

        for (int i = 0; i <10 ; i++) {//循环创建10个线程
            Thread t = new Thread(td);
            t.start();
        }
    }
}

CAS 理解为一种自旋思想

CAS(Compare-And-Swap) 比较 并 交换 适用于 低并发情况下.

CAS 是乐观锁的一种实现方式,他采用的是自旋锁的思想,是一种轻量级的锁机制.

乐观锁 指的是一种不加锁就可以解决的方式

自旋锁 (轮询) 一直不停的循环检查

CAS 包含了三个操作数:

①内存值 V

②预估值 A (比较时,从内存中再次读到的值)

③更新值 B (更新后的值)

当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。

在这里插入图片描述

这种做法不会导致线程阻塞,因为没有加锁

缺点:

在高并发情况下,采用自旋方式去不断的循环, 会对cpu占用率太高.

可能会产生ABA问题 可以为类添加版本号的方式区别 值是否被修改过.

JUC常用类

public class HashMapDemo {

    /*
       HashMap是线程不安全的,不能并发操作的
       ConcurrentModificationException  并发修改异常   遍历集合,并删除集合中的数据

       Hashtable 是线程安全的 public synchronized V put(K key, V value)-->独占锁
            锁直接加到了put方法上,锁粒度比较大,效率比较低
            用在低并发情况下可以

       Map<String,Integer> map = Collections.synchronizedMap(new HashMap<>());
       ConcurrentHashMap
     */
    public static void main(String[] args) {

        ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap<>();
        //模拟多个线程对其操作
        for (int i = 0; i < 20; i++) {
                 new Thread(
                     ()->{
                       map.put(Thread.currentThread().getName(), new Random().nextInt());
                         System.out.println(map);
                     }
                 ).start();
        }

    }
}

ConcurrentHashMap

ConcurrentHashMap采用锁分段机制,并没有将整个hash表锁住, jdk8之后没有使用分段锁(给每个位置创建一个锁标志对象).

采用的是CAS思想+synchronized来实现

插入是,检测hash表对应位置是否是第一个节点, 如果是采用CAS机制(循环检查) 向第一个位置插入数据.

如果此位置已经有值,那么就以第一个Node对象为锁标志进行加锁,使用的是synchronized实现.

java中的锁

有许多锁的名词

有的指锁的特性,有的指锁的设计, 有的指锁的状态

乐观锁: 乐观锁就是不加锁, 认为并发的修改是没有问题的, 例如CAS机制 是一种无所的方式实现.

悲观锁: 认为并发操作会出现问题, 需要通过加锁来保证安全. 适合写操作多

可重入锁

当一个同步方法中,调用另一个和他使用同一把锁的方法时,

在外层方法中,即使锁没有释放的情况下,也可以进入另一个同步方法

Synchronized和ReentrantLock都是可重入锁

**读写锁(**ReadWriteLock)

是具体的锁实现 读和写是两把锁 进行分离使用.

分段锁

不是一种锁实现, 是一种加锁的思想, 采用分段加锁,降低锁的粒度. 从而提高效率

自旋锁

不是一种锁实现, 采用自旋(循环重试)的方式进行尝试获取执行权. 不会让线程进入到阻塞的状态,

适用于锁的时间较短的情况.

共享锁: 可以被多个线程共享的锁

​ ReadWriteLock 的读锁是共享. 多个线程可以同时读数据

独占锁: 一次只能有一个线程获取锁

​ ReentrantLock,Synchronized,ReadWriteLock 写锁都是独占锁.

ReadWriteLock 里面实现方式 使用一个同步队列, 例如读线程获取资源, 将标准state设置为已被使用,然后将其他写线程加入到一个对列中等待.

AQS(AbstractQueuedSynchronizer)

维护一个队列,让等待线程排队.

公平锁: 就是会维护一个线程的等待队列,依次去执行线程

​ ReentrantLock默认是非公平, 可以在创建时通过构造方法为其指定是公平锁还是非公平锁

非公平锁: 没有队列,一旦锁释放,线程开始抢占,谁抢到执行权谁先执行 synchronized就是非公平的

锁的状态

无锁/ 偏向锁/轻量级锁/重量级锁

偏向锁: 只的是一直只有一个线程在不断的获取锁, 可以更方便的获取到锁

轻量级锁: 当时偏向锁时, 再有一个线程来进行访问,那么锁状态升级为轻量级锁

​ 如果是轻量级锁,那么等待的线程不会进入阻塞状态,采用自旋方式重新尝试获得锁, 效率提高.

重量级锁: 当锁状态为轻量级锁时, 如果有的线程自旋次数过多, 或者有大量的线程访问

​ 那么,锁状态升级为重量级锁, 此时未获得锁的线程不再自旋,进入到阻塞状态.

Synchronized

是可重入锁,非公平锁

是关键字,可以修饰代码块,也可以修饰方法

是隐式的 自动获取 释放锁.

Synchronized 实现加锁 释放锁 是指令级别的

有一个进入监视器 +1 对象头锁标记被使用

​ 执行任务

退出监视器 -1 0 对象头锁标记被改为无锁

ReentrantLock

是类 只能修饰代码块 是显示的, 手动添加 手动释放

是类层面是实现控制的,采用 CAS+AQS 是可重入锁. 可以是公平锁,也可以不是公平锁.

在类的内部自己维护了一个锁的状态, 一旦有线程抢占到了,将状态改为1,其他线程进入到队列中等待锁的释放,

锁一旦释放了,那么就唤醒头结点 开始尝试去获得锁.

线程池

池 数据库连接池 每次与数据链接, 创建连接对象Connection 操作完之后,进行销毁 频繁创建销毁比较耗时.

​ 创建一个池子,预先在池子中初始化好一部分连接(Connection )对象, 使用时直接获取即可,用完还回,不需要频繁创建销毁.

为什么使用线程池?

并发量大的情况下,频发创建销毁线程开销较大.

创建线程池缓解压力.

jdk5之后,提供ThreadPoolExecutor类来实现线程池创建, 是建议被使用的, 里面有7个参数来设置对线程池的特征的定义.

ThreadPoolExecutor 继承了 AbstractExecutorService 类,并提供了四个构造 器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调 用的第四个构造器进行的初始化工作。

构造器中各个参数的含义

7个:

corePoolSize: 核心线程池数量, 在创建后核心线程池数量默认为0, 有任务来了后,才会去创建线程去执行,

​ 或者调用prestartAllCoreThreads()或者 prestartCoreThread()方法,进行预创建.

maximumPoolSize: 线程池总数量,表示线程池最大能装多少个线程.

keepAliveTime: 指的是非核心线程池中的线程,在没有任务执行时空闲多长时间后内销毁.

unit:为keepAliveTime定义时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性:

​ TimeUnit.DAYS, //

​ TimeUnit.HOURS, //

​ TimeUnit.MINUTES, //

​ TimeUnit.SECONDS, //秒

​ TimeUnit.MICROSECONDS, //微秒

​ TimeUnit.MILLISECONDS, //毫秒

​ TimeUnit.NANOSECONDS, //纳秒

workQueue: 等待队列 可以自己来指定等待队列的实现类.

threadFactory:线程工厂,主要用来创建线程;

handler:表示当拒绝处理任务时的拒绝策略

package threadpool;

import java.util.concurrent.*;

public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,
                                                         6,
                                                          200,
                                                      TimeUnit.MILLISECONDS,
                                        new ArrayBlockingQueue<>(2),
                                           Executors.defaultThreadFactory(),
                              new ThreadPoolExecutor.DiscardOldestPolicy());

        for(int i=1;i<=10;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//添加任务到线程池

        }
        executor.shutdown();
    }
}

线程池的执行

当有任务提交到线程池时

首先检测核心线程池是否已满:

​ 未满: 在核心线程池创建一个线程处理

​ 已满: 将任务添加到一个等待队列中

继续有任务提交过来, 核心线程池已满, 等待队列已满, 这时就要创建非核心线程来处理任务,

如果任务继续增多,核心线程池,等待队列,非核心线程池都已满, 那么使用相应的拒绝策略处理.

拒绝策略:

AbortPolicy: 抛出异常

DiscardOleddestPolicy 策略:该策略将丢弃最老的一个请求

DiscardPolicy 策略:该策略丢弃无法处理的任务,不予任何处理

CallerRunsPolicy: 让提交任务的线程去执行 例如我们的main线程

package threadpool;

import java.util.concurrent.*;

public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,
                                                         6,
                                                          200,
                                                      TimeUnit.MILLISECONDS,
                                        new ArrayBlockingQueue<>(2),
                                           Executors.defaultThreadFactory(),
                              new ThreadPoolExecutor.DiscardOldestPolicy());
                              //new ThreadPoolExecutor.AbortPolicy());
                              //new ThreadPoolExecutor.DiscardPolicy());
                              //new ThreadPoolExecutor.CallerRunsPolicy());
        for(int i=1;i<=10;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//添加任务到线程池

        }
        executor.shutdown();
    }
}

execute 与 submit 的区别

都是向线程池提交任务的

execute 没有返回值

submit可以接收返回值

线程池关闭

关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现。

shutdownNow: 立即终止线程任务

shutdown : 不接收新的任务 执行完任务关闭

创建线程有几种方法:4

继承Thread

实现Runnable

实现Callable

使用线程池

ThreadLoacl

线程变量

为每个线程保存一个变量副本,使得多个线程间变量相互不影响.

ThreadLocal 原理分析

创建一个ThreadLocal对象

调用set方法时, 会在底层 获取到当前正在执行的线程对象, 为我们的当前线程创建ThreadLocalMap对象

ThreadLocalMap的键是ThreadLocal对象 , 值就是我们自己set的值

找的时候,先通过我们自己线程去找对象的ThreadLocalMap

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
public class Demo {

    static  int num = 0;

    public static void main(String[] args) {
         new Thread(){
             @Override
             public void run() {
                 num++;
                 System.out.println(num);
             }
         }.start();
        new Thread(){
            @Override
            public void run() {
                num++;
                System.out.println(num);
            }
        }.start();
    }
}


public class ThreadLocalDemo {

    //创建一个ThreadLocal对象,用来为每个线程会复制保存一份变量,实现线程封闭
    private  static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };


    public static void main(String[] args) {
          new Thread(){
              @Override
              public void run() {
                   localNum.set(1);
                  try {
                      Thread.sleep(2000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  localNum.set(localNum.get()+10);
                  System.out.println(Thread.currentThread().getName()+":"+localNum.get());//11
              }
          }.start();

        new Thread(){
            @Override
            public void run() {
                localNum.set(3);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                localNum.set(localNum.get()+20);
                System.out.println(Thread.currentThread().getName()+":"+localNum.get());//23
            }
        }.start();

        System.out.println(Thread.currentThread().getName()+":"+localNum.get());//0
    }
}

ThreadLocal 内存泄漏问题

内存泄漏: 就是有些对象在内存中已经不被使用,但是不能被回收的对象

ThreadLocalMap的键是ThreadLoacl, 是一个被弱引用对象管理的.

如果长时间的线程执行,那么键是弱引用, 键就被回收了为null,

但是value 可能还一直被占用着,是强引用. 不能被回收掉, 造成内存泄漏

解决办法: 在ThreadLoacl中的变量被使用完成后 立即将其删除

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值