并发
- 同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单地说,单核cpu实现的多任务就是并发
并行
- 同一个时刻,多个任务同时执行。多核cpu可以实现并行
创建线程的两种方式
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
- java是单继承的,在某些情况下一个类可能已经继承了某个父类。这是在用继承Thread类方法类创建线程显然不可能了。
- Java设计者们提供了另一个方式创建线程,就是通过实现Runnable接口来创建线程
线程终止
- 当线程完成任务后,会自动退出
- 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式。
线程常用方法
- setName 设置线程名称,使之与参数Name相同
- getName 返回该线程的名称
- start 使该线程开始执行, Java虚拟机底层调用该线程start0方法
- run 调用线程对象run方法
- setPriority 更改线程的优先级
- getPriority 获取线程的优先级
- sleep 在指定的毫秒数内让当前正在执行的线程休眠
- interrupt 中断线程
- yieId 线程的礼让,让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
- join 线程的插队。插队的线程一旦插队成功,则坑定先执行完插入的线程所有的任务
用户线程和守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
- setDaemon() 把一个线程设置为守护线程
线程的生命周期
- NEW 尚未启动的线程处于此状态
- RUNNABLE 在Java虚拟机中执行的线程处于此状态
- BLOCKED 被阻塞等待监视器锁定的线程处于此状态
- WAITING 正在等待另一个线程执行特定动作的线程处于此状态
- TIMED_WAITING 正在等待另一个线程执行动作达到指定等待的时间的线程处于此状态
- TERMINATED 已退出的线程处于此状态
线程同步
- 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据完整性
- 也可以这样理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
Synchronized
- 同步代码块
synchronized(对象) { // 得到对象的锁,才能操作同步代码
// 需要被同步才买;
}
2、synchronized还可以放在方法声明中,表示整个方法-为同步方法
pubilc synchronized void m (String name) { // 需要被同步的代码
}
- 线程获取锁
- 清空变量副本
- 拷贝共享变量最新的值到变量副本中
- 执行代码
- 将修改后的变量副本中的值赋值给共享数据
volatile关键字
强制线程每次在使用的时候,都会去看一下共享区域最新的值。
- 不能保证原子性
互斥锁
-
Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
-
每个对象懂对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问对象
-
关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
-
同步的局限性:导致程序的执行效率要降低
-
同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
-
同步方法(静态的)的锁为当前类本身
-
注意事项
1.同步方法如果没有使用static修饰:默认锁对象为this
2.如果方法使用static修饰,默认对象:当前类.class
3.实现的落地步骤:
- 需要先分析上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个即可
线程死锁
- 多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生
释放锁
- 当前线程的同步犯法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、return
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法、当前线程暂停,并释放锁。
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yeild()方法暂停当前线程的执行,不会释放锁
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。 尽量避免使用suspend()和resume()来控制线程,方法不在推荐使用。
线程池
java中经常需要用到多线程来处理一些业务,我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
- Executors创建线程池
| 方法名 | 功能 |
|---|---|
| newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
| newSingleThreadExecutor() | 创建只有一个线程的线程池 |
| newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
// 提交任务
.submit(new Theade);
// 关闭线程池
.shutdown();
/*参数列表*/
// 1. 核心线程数量- 数量不能小于0
// 2. 最大线程数量- 不能小于0,最大数量 大于等于 核心线程数量
// 3. 空闲线程最大存活时间- 不能小于0
// 4. 时间单位- 时间单位
// 5. 任务队列- 不能为null
// 6. 创建线程工厂- 不能为null
// 7. 任务拒绝策略- 不能为null
new ThreadPoolExecutor(
2,
5,
2,
TimeUnit.HOURS,
new ArrayBlockingQueue(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
- 任务的拒绝策略
ThreadPollExecutor.AbortPolicy; 丢弃任务并抛出Rejected-Execution-Exception异常,
是默认策略
ThreadPollExecutor.DiscardPolicy; 丢弃任务,但是不抛出异常,这是不推荐的做法。
ThreadPollExecutor.DiscardOldestPolicy; 抛弃队列中等待最久的任务,然后把当前任务加入队列中。
ThreadPollExecutor.CallerRunsPolicy; 调用任务的run()方法绕过线程池直接执行。
原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会收到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可分割的整体。
-
原子类AtomicInteger
-
public AtomicInteger(); 初始化一个默认值为0的原子型Integer
-
public AtomicInteger(int); 初始化一个指定值的原子型Integer
方法 -
int get(); 获取值
-
int getAndIncrement(); 以原子方式将当前值加一,注意这里返回的是自增前的值
-
int incrementAndGet(); 以原子方式将当前值加一,注意这里返回的是自增后的值
-
int addAndGet(int data); 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
-
int getAndSet(int value); 以原子方式设置为newValue的值,并返回旧值。
-
-
AtomicInteger原理 自旋锁 + CAS算法
CAS算法:有3个操作数(内存值V,旧的预期值A,需要修改的值B)
当旧的预期值A == 内存值 此时修改成功,将V修改为B
当旧的预期值A != 内存值 此时修改失败,不做任何操作
并重新获取现在的最新值(这个重新获取的动作就是自旋),然后再次操作。 -
synchronized和CAS的区别 相同点:在多线程情况下,都可以保证共享数据的安全性
不同点:synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每次操作共享数据之前,都会上锁(·悲观锁·)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁,只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。
ConcurrentHashMap
保证线程安全,同时兼顾效率
- JDK 1.7底层原理
- 创建一个默认长度为16,默认加载因子为0.75的数组,数组名Segment
- 再创建一个长度为2的小数组,把地址值赋值给0索引,其他索引都是null
- 第一次会根据键的哈希值类计算出在大数组中应存入的位置,如果为null,则按照模板创建小数组,创建完毕会二次哈希,计算出在小数组中应存入的位置,直接存入。
- 如果不为null,就会根据记录的地址值找到小数组,二次哈希,计算出小数组中应存入的位置。如果需要扩容,则将小数组扩容两倍。如果不需要扩容,则判断小数组这个位置有没有元素,如果没有元素则直接存。如果有元素,就会调用equals方法,比较属性值,如果equals为true,则不存。如果equals为false,则形成链表结构。
- JDK 1.8底层原理
- 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做,在第一次添加元素的时候创建哈希表。
- 计算当前元素应存入的索引。
- 如果该索引位置为null,则利用cas算法,将本节点添加到数组中。
- 如果该索引位置不为null,则利用volatile关键字获得当前位置最新的节点地址,挂在它下面,变成链表。
- 当链表的长度大于等于8时,自动转换成红黑树。
- 以链表或者红黑树头节点为锁对象,配合悲观锁保证多线程操作集合时数据安全性。
CountDownLatch
| 方法 | 解释 |
|---|---|
| public CountDownLatch(int count) | 参数传递线程,表示等待小城数量 |
| public void await() | 让线程等待 |
| public void CountDown() | 当线程执行完毕 |
使用场景:让某一条线程等待其他线程执行完毕之后再执行。
CountDownLatch(int count): 参数写等待线程的数量,并定义了一个计数器。
await(): 让线程等待,当计数器为0时,会唤醒等待的线程。
countDown(): 线程执行完毕时调用,会将计数器-1;
Semaphore
使用场景:控制访问特定资源的线程数量
-
创建Semaphore对象。
-
acquire() 发通行证
-
release() 收回通行证