Java多线程学习笔记
Java多线程
0x00 重写 vs 重载 | 并发 vs 并行
重载:
- 重载(Overload)发生在一个类中,极大限度地发挥了Java面向对象时的静态多样性,是 编译时的多态性
- 重载是符合实际情况的,比如一个动作(方法),如打电话,他们的所需条件(参数)是手机是相同的,做出的行为(返回值)是讲话,也是相同的,但是他们可能在讲话中(方法具体实现)是不同的,好比与上司打电话和同事之间打电话一样,可能有相同点,但肯定也有不同点,同事之间可以开玩笑,语气诙谐,所以打电话这个方法不能对所有对象采取一样的行为,就要用到重载。
- 重载是Java在编译的时候根据 参数的不同(类型以及数量) 来区分开的,不根据返回值进行区分,返回值可以相同也可以不同
重写(覆盖):
重写(Overrides)发生在父类和子类之间,发挥了Java面对对象语言的动态多样性, 是运行时的多态性
- 子类继承父类的方法之后可能还需要有自己独立的,不同于父类的方法,我们就要使用重写,类似于现实生活中的基因突变或者说遗传变异
- 重写的父类一般都是高度抽象出的一个类,下面分为有相同的属性和自己独特属性很多个小类,比如图书馆借书的人,下面可能就有学生和老师两个类继承他,学生和老师都能借书所以有共同的属性,但是他们的待遇不同所以有自己独特的一面需要重写
- 重写绝大部分情况下都是要包含父类的方法的——不能“数典忘祖”,所以在类中多要用到
super.method();
。倘若没有用到,说明是否这个父类有点多余,因为他没有体现出一个高度抽象的父类的一个共同的特点 - P.S. 在继承中,父类的所有公开(public)和保护(protected)的属性与方法,子类是可以看见的并使用调用的,但是,父类的私有(private)属性和方法,子类是无法使用的,放到现实生活中我们也可以很清楚的看出,你父亲的年龄(private)你自己是无法使用的,但是你可以有自己的年龄,二者不会冲突。
- 在构造的时候,类可以向下转换但不能向上转换
public static void main(String[] args) { Father son1 = new Son(); //合法的 Son son2 = new Father(); //不合法的,编译器报错 }
具体而言是因为:子类完全继承了父类非私有的所有方法和属性,所以父类有的,子类一定也有;但是父类不一定具有子类的所有方法,所以当一个子类用父类的构造函数的时候,就会出现有很多属性缺失的情况,这是不允许的,总结来说就是 宁可冗余,也不能缺少
- 静态的方法不能被重写为非静态的方法,编译器会报错
重写方法的规则:
- 参数列表与返回值的类型必须完全与被重写的方法相同,否则不能称其为重写而是重载。
- 访问修饰符的限制一定要大于等于被重写方法的访问修饰符** (public>protected>default>private)**
- 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常,即子类的重写方法只能抛出父类抛出的更低等级的异常。例如:
父类的一个方法申明了一个检查异常IOException
,在重写这个方法是就不能抛出Exception
,只能抛出IOException
的子类异常,可以抛出非检查异常
//坑:检查异常 vs 非检查异常
0x01
不可变对象不存在线程安全问题,因为他不会共享,因为每次都会被生成一个全新的对象,用final关键字限制
所以在多线程中多使用不可变对象
无状态对象不存在线程安全问题,因为他不会将错误状态共享
0x02
给方法加锁 == 给对象加锁 synchronize this;
同时有有状态对象和无状态对象时候,应该给有状态对象加锁,而不是无状态对象
0x03
竞争模式是不线程安全的
- Read-modify-write
- check-then-act
上面两种方法都要进行多步才能完成,执行步骤之间可能会有线程进来,破坏原子性
如:count++ 是不安全的,因为需要3步:- Read count
- Modify count = count + 1
- Write count (Update)
而count = 1的赋值语句是安全的
对于count++而言,其实java提供了原子性的方法: Atomic**
如:count++ <--> incrementAndGet(count)
源码展示:
// 封装了一个int对其加减
private volatile int value;
.......
public final boolean compareAndSet(int expect, int update) {
// 通过unsafe 基于CPU的CAS指令来实现, 可以认为无阻塞.
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
.......
public final int getAndIncrement() {
for (;;) {
// 当前值
int current = get();
// 预期值
int next = current + 1;
if (compareAndSet(current, next)) {
// 如果加成功了, 则返回当前值
return current;
}
// 如果加失败了, 说明其他线程已经修改了数据, 与期望不相符,
// 则继续无限循环, 直到成功. 这种乐观锁, 理论上只要等两三个时钟周期就可以设值成功
// 相比于直接通过synchronized独占锁的方式操作int, 要大大节约等待时间.
}
}
0x04
给什么加锁:
给方法体加锁 = 给this加锁
给对象加锁synchronized (object)
,可以使得锁的块大小更小,当object为自己时,和对this加锁无异。
为了效率的问题,建议采用对对象加锁,这样同步的块变少,执行效率更高
0x05
notify和notifyAll的区别
- notify是随机唤醒一个线程,而notifyAll是唤醒所有线程。比如:让某一个线程去做事,而不想让所有线程去抢着做一件事,用notify,相当于抽签的形式。
0x06
用Lock代替notify
当我们的生产者和消费者都是多个的时候,notify随机唤醒可能会唤醒本方,导致对方锁不放,此时不如用Lock来确定每一方的锁
0x07 对于Java多线程经典生命周期图的理解
- 首先我们可以发现:在调用了
start()
函数后只是转移到了Runnable
,使其具备可以运行的能力,但是并没有真正运行,还需要 获取CPU ,这里就是有关操作系统内部的CPU调度问题了,CPU会将时间切片 公平地(fair)——根据先后时间顺序分配,或者 非公平地(fairless)——随机分配 给线程,只有拿到CPU执行的线程才能运行。
- 在
Running
之后,我们的线程可能因为某个原因变成 阻塞状态(Blocked),具体而言分为三种:- 等待阻塞: 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞: 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
0x08 线程共享变量的三种方式
主函数
public class VolatileTest {
public static int number = 0;
public void increase(){
//占坑下面填写
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
test.increase();
}
}).start();
}
//若当期依然有子线程没有执行完毕
while(Thread.activeCount() > 1){
Thread.yield();//使得当前线程(主线程)让出CPU时间片
}
System.out.println("number is " + number);
}
}
- synchronize (强制要求内部代码执行前后均同步)
具体步骤为以下:
* a. 线程获得互斥锁
* b. 清空工作内存
* c. 从主内存拷贝共享变量最新的值到工作内存成为副本
* d. 执行代码
* e. 将修改后的副本的值刷新回主内存中
* f. 线程释放锁
public void increase(){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(this){
number++;
}
}
- ReentrantLock (显式地给线程上锁)
```java
public class VolatileTest {
public static int number = 0;
public Lock lock = new ReentrantLock();
public void increase(){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try{
number++;//这块的代码实际项目中可能会出现异常,所以要捕获
}finally{
lock.unlock();//用try finally块保证Unlock一定要执行
}
}
}
>3. AtomicInteger (Java自带的函数,保证操作为原子的)
```Java
package com.mooc.test;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest {
public static AtomicInteger number = new AtomicInteger(0);
public void increase(){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
number.getAndIncrement();//获得当前值并且加1
}
/**
* @param args
*/
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
test.increase();
}
}).start();
}
//若当期依然有子线程没有执行完毕
while(Thread.activeCount() > 1){
Thread.yield();//使得当前线程(主线程)让出CPU时间片
}
System.out.println("number is " + number.get());
}
}
<p data-line="209" class="sync-line" ></p>
0x09
如何在List中安全地删除Object?
// 迭代删除方式四
for (Iterator<String> ite = list.iterator(); ite.hasNext();) {
String str = ite.next();
System.out.println(str);
if (str.contains("b")) {
ite.remove();
}
}
<p data-line="223" class="sync-line" style="margin:0;"></p>
用iterator的方式删除最为保险
0x0a
多线程什么时候会释放锁?
假设现在线程A获得了锁的权限(synchronized (obj)
),然后用wait()
方法的时候,线程A会进入Blocked
的等待序列,同时会释放obj
的锁,共给其他线程进入,然后再在其他进程中通过notify() 或 notifyAll()
来唤醒线程A,当线程A被唤醒之后,只有等待到下次CPU分配给线程A且线程A得到锁之后,A才能从wait()
醒来的地方继续执行。
Reference: