Java 线程与并发编程
一、线程
1.什么是线程,进程
进程:进程是操作系统分配资源的基本单位
线程:线程是进程中的一个执行单元(一个独立执行的任务),是cpu执行的最小单元

2.java中如何创建线程
1.线程的四种创建方式
在 Java 中,创建线程主要有四种方式,适用于不同场景:
继承 Thread 类:重写run方法,直接创建子类对象并调用start启动。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 使用:new MyThread().start();
实现 Runnable 接口:重写run方法,通过new Thread(任务对象)创建线程。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 使用:new Thread(new MyRunnable()).start();
实现 Callable 接口:与 Runnable 相比,Callable 可以返回结果并抛出异常,配合 Future 获取结果。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 100;
}
}
// 使用:Future<Integer> future = new ThreadPoolExecutor().submit(new MyCallable());
线程池:通过ThreadPoolExecutor创建线程池,复用线程资源(后文详细讲解)。
2.线程中常用的方法
run() ,call()
Thread类中的方法:
start():启动线程的,把线程注册到操作系统 private native void start0();
sleep():让线程休眠指定的时间
join():让其他线程等待当前线程执行完成后再执行.
yeild():线程礼让
setDaemon():设置线程为守护线程
currentThread(): 获得当前正在执行的线程
wait():让线程等待,只能被其他线程唤醒
notify(),notifyAll():唤醒被wait等待的线程
3.线程的状态

3.多线程
1.什么是多线程
在程序中(进程)可以创建多个线程来分别执行不同的任务
2.多线程的优缺点
优点: 可以提高程序执行效率
缺点: 线程多了,需要操作系统进行管理的,占用开销的(不是啥事情都创建线程执行的.)
多个线程同时访问共享资源(数据)会出现问题
3.线程同步
加锁排队 (饭堂买饭 排队+加锁 一次只能有一个买饭)
synchronized关键字
synchronized修饰方法 非静态方法锁对象是this 静态方法是类的Class对象
synchronized修饰代码块
ReentrantLock
public class ReentrantLockDemo implements Runnable{
int num = 10;
/*
ReentrantLock是java.util.concurrent.locks包下的类,
是java代码实现的一种锁控制
只能手动的加锁和手动的释放锁
只能对某段代码块加锁,不能给整个方法加法
*/
ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
reentrantLock.lock();//加锁
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "买到第" + num + "张票");
num--;
} else {
break;
}
}finally {
reentrantLock.unlock();//释放锁
}
}
}
}
4.死锁
多个线程相互持有对方需要的锁对象不释放,二形成的一种相互等待获取锁的现象,
死锁发生后,程序不会报错,只能相互等待,不继续向后执行了.
如何避免死锁发生:
避免锁的嵌套使用
避免多个同步代码块中的锁相互使用
口语描述死锁:
线上1和线程2 同时访问两个代码块, 线程1访问的同步代码块使用A锁,在A锁的同步代码块又使用了B锁.
线程2在访问的同步代码块中使用B锁,在B锁的同步代码块中又用到了A锁,有可能形成死锁
5.线程通信(生产者,消费者模型)
在线程同步的基础上进行的,两个线程相互牵制执行
wait(); 线程等待
notify(); 唤醒等待的线程
只能在同步代码块(同步方法)中使用, 还只能通过同步锁对象调用
案例:交替打印数字
public class PrintNum implements Runnable{
int num = 0;
Object obj = new Object();
@Override
public void run() {
while (true){
synchronized(obj){
obj.notify();
System.out.println(Thread.currentThread().getName()+":"+num++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
wait()和sleep()区别:
所属的类不同: wait()属于Object类 sleep()属于Thread类
阻塞后唤醒方式不同: wait()让线程等待后,需要让另一个线程通过notify()唤醒,如果不唤醒一直等待
sleep()休眠执定的时间,时间到了后,自动唤醒
释放锁不同: sleep()不会释放锁
wait()会释放锁
二、并发编程
后面谈论的线程内容,几乎都是在多线程对同一共享数据操作的基础上进行.
1.多线程优缺点
优点: 可以提高程序执行效率
缺点: 线程多了,需要操作系统进行管理的,占用开销的(不是啥事情都创建线程执行的.)
多个线程同时访问共享资源(数据)会出现问题
并发(高并发): 计算机领域指的是同一时刻只能有一个任务执行,多个任务依次执行
汉语并发指的是同时执行
并发执行: 在很多用户同时访问时,应当让多个用户并发执行(一个时间段内依次执行)
并行执行: 在同一个时间点上,多个任务同时执行
2.并发编程核心问题
多个线程同时对同一个资源访问的情况下. 为什么会出现问题.
2.1不可见性
首先了解java程序运行的内存模型(JMM)
java在线程操作时,先把主内存中的数据加载到线程的工作内存中,然后对数据进行操作,但是线程A在自己的工作内存中操作玩出,线程B不知道,做了同样的操作,最终结果和预期结果不一样

2.2乱序性(无序性)重排序
系统为了优化,在执行某些指令时,会将一些看起来没关系的指令执行顺序改变,但是在,某些情况下会产生问题.
1 int a = 10;
2 int b = 从数据库读取
3 int c = a+b;

2.3非原子性
i++;在多线程情况下时线程安全的吗?
i++高级语言,在底层执行时,会被分为3条件`
加载i
i+1
i=2
线程的切换执行会带来原子性问题
java内存模型为每个线程提供工作内存(缓存),导致不可见性问题
系统指令优化会把一些指令顺序重排序(在执行一些较为耗时的指令时,把一些其他指令先行执行),可能导致程序无法正常执行
线程切换执行会打破指令执行的原子性 ,例如++操作 分为三条指令, 但是在执行这3条指令时,cpu可能在执行时,切换到其他线程执行,打破原子性执行.
2.4如何解决以上3个问题:
2.4.1 volatile
volatile关键可以解决不可见性和乱序性
volatile修饰的共享变量,在被一个线程操作后,可以做到立即对其他线程可见 (volatile底层实现 内存屏障)
volatile修饰的变量禁止对其的执行顺序重排序,volatile只能解决不可见性和重排序问题,不能解决非原子性问题
2.4.2 如何解决非原子性问题:
1. 加锁(ReentrantLock和synchronized)
2. 使用原子类来解决++在多线程中非原子性问题
AtomicInteger 是一个原子类,能够在不加锁的情况下,实现多线程++操作不出问题,结果正确
private static AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet(); 自增并获得
2.4.3 CAS 机制
CAS(Compare-And-Swap) :比较并交换
特点: 不加锁实现对变量++操作保证原子性.
优点: 线程不会进入到阻塞状态,一直自旋,效率高
缺点: 线程一直自旋,对cpu开销大, 所以原子类适用于线程少的情况
3.Java 中的锁分类
3.1乐观锁/悲观锁
乐观锁: 就是没有加锁的实现. AtomicInteger中的实现就是不加锁的,通过自旋比较实现(CAS)
悲观锁: 就是加锁的实现,认为不加锁是会出问题的 ,ReentrantLock和synchronized都是悲观锁
3.2可重入锁
ReentrantLock和synchronized都是可重入锁
可重入锁又名递归锁, 指的是一个线程在外层方法获得锁时,可以直接进入到内层的加锁的方法中.

3.3自旋锁
指的是对synchronized获得锁的一种描述(特点), 线程在获得锁时,是自旋的不断尝试去获得锁
3.4公平锁/非公平锁
公平锁: 就是排队获得锁,有先来后到 ReentrantLock 既可以是公平锁也可以是非公平锁
非公平锁: 就是抢锁,谁抢到谁执行, 有可能后来的线程先抢到锁 synchronized ReentrantLock
3.5读写锁
ReentrantReadWriteLock
特点: 读读不互斥, 读写互斥, 写写互斥
适合读(查询)多,写少的场景, 提高读的效率

3.6共享锁和独占锁
独占锁: synchronized ReentrantLock都是独占锁,就是有我没他,一次只能有一个线程执行.
共享锁: 一个锁可以被多个线程持有, 读写锁中的读锁就是共享锁
4.synchronized锁的实现
jdk1.7之后,对synchronized锁进行了优化(jdk7之前synchronized锁没有状态,都是自旋的获取锁),jdk7之后为synchronized锁设计了不同的状态.
无锁状态: 没有线程进入到同步代码块就是无锁状态
偏向锁状态: 只有一个线程访问同步代码块时,同步锁中记录线程id,下次线程访问时,可以快速的获得锁.
轻量级锁状态: 当线程数量大于1个之后,锁状态由偏向锁升级为轻量级锁, 线程不会阻塞,以自旋方式获得锁,提高获取锁的效率.
重量级锁状态: 当锁状态为轻量锁时,如果线程自旋到一定次数还获取不到锁,那么锁会升级为重量级锁,让获取不到锁的线程进入到阻塞状态,等待操作系统调度.
使用synchronized锁的时候,必须为锁提供一个同步锁对象的,此对象就是用来记录锁状态的
对象中有一个区域叫对象头,对象头中有一块区域叫mark word,记录对象运行时的一些数据,如锁状态,哈希值,GC分代年龄,当前线程id.

synchronized是java中内置的一种的锁,底层实现是靠底层指令进行控制的,


使用时必须提供一个同步锁对象,用来记录锁的各种状态.
5.AQS
AbstractQueuedSynchronizer 抽象同步队列, 并发包下面很多类的底层实现都会用到
内部有一个int类的变量state,用来记录有没有线程使用
内部会构建一个队列,用来存储没有获得锁的线程




6.ReentrantLock锁实现
ReentrantLock 基于 AQS的,
ReentrantLock 可以实现公平锁和非公平锁
内部结构



公平和非公平的区别



7.线程池
7.1理解池的概念
字符串常量池 String s1 = "abc"; String s2="abc"; s1==s2;
数据库连接池 Connection 每次链接创建,每次用完销毁 创建对象是要花费时间的
Integer -128---+127也有缓存池,当定义一个Integer类型的变量时,如果数值大小在-128---+127直接,那么该变量则会直接在缓存池中引用,否则才会new出新的对象。
int a=17;int b=17;那么a==b为true;
int a=170;int b=170;那么a==b为false;

7.2 线程池
package com.threadPool;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());
for(int i=1;i<=8;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);//添加任务到线程池
}
executor.shutdown();
}
}
package com.threadPool;
public class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕"+":"+Thread.currentThread().getName());
}
}
如果每次执行任务时,都去创建线程对象,用完销毁,频繁的创建也是比较占资源的.
jdk5之后,提供ThreadPoolExecutor类来实现线程池.
好处: 避免了频繁的创建也是比较占资源的, 统一管理线程
ThreadPoolExecutor构造方法中的7个参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize: 核心线程池数量
maximumPoolSize: 最大线程池数量
keepAliveTime: 非核心线程池中的线程, 空闲多久后销毁
unit: 为keepAliveTime指定时间单位
workQueue: 等待的线程队列额
threadFactory: 创建线程的工厂
RejectedExecutionHandler: 拒绝策略
7.2.1线程池的执行流程

7.2.2线程池中的队列
线程池有以下工作队列:
ArrayBlockingQueue
LinkedBlockingQueue
7.2.3线程池的拒绝策略
1.AbortPolicy 直接抛出异常

2.CallerRunsPolicy 拒绝后,由提交任务的线程执行此任务(如main线程)

3.DiscardOldestPolicy 丢弃队列中等待时间最长的那一个

4.DiscardPolicy 丢弃最后来的无法执行的任务

7.2.4向线程池提交任务的两种方法
execute 与 submit 的区别
void execute 适用于不需要关注返回值的场景
submit 方法适用于需要关注返回值的场景。
7.2.5关闭线程池
shutdownNow 立刻关闭,即使还有未执行完的任务
shutdown 等待所有任务执行完了再关闭
8.ThreadLocal
作用: 为每个线程提供一个变量副本
使用:
//创建ThreadLocal对象,为每个线程自动的提供一个变量副本
static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1; //初始化变量
}
};
public static void main(String[] args) {
/*
这种方式在两个线程中对num进行操作,这个num是同一个
*/
//线程1
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(10);
threadLocal.set(threadLocal.get()+5);
System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
}
}).start();
//线程2
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(20);
threadLocal.set(threadLocal.get()+10);
System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
}
}).start();
}
内部实现

ThreadLocal 内存泄漏问题
内存溢出: 内存不够用了
内存泄漏: 一些对象已经不再使用,但是虚拟机又不能回收的对象( 例如: 数据库连接对象,IO流对象,Socket 提供close)
如果使用ThreadLocal不当,会造成内存泄漏问题。
对象与引用关系
Object obj = new Object(); 强引用
obj= null; 没有引用
软引用: 被SoftReference对象管理的引用, 内存充足时,不回收该对象,一旦内存不足时,
就会回收软引用管理的对象.
Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<Object>(o1);
弱引用: 被WeakReference对象管理的引用, 只要进行垃圾回收,就会被回收掉
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<Object>(o1);
ThreadLocal被弱引用管理的, 下次垃圾回收到来时,ThreadLocal会被回收掉, 造成ThreadLocalMap中的不存在了,
但是value还被外界引用, 所以ThreadLocalMap就不能被回收, 造成了内存泄漏.
所以, 正确的使用ThreadLocal方式是在用完之后, 主动删除ThreadLocalMap中的数据.
更多推荐




所有评论(0)