主要用来记录一些自己觉得重点的知识
不定期更新读书笔记 欢迎关注、点赞
[TOC]
马桶Java 上厕所就能看完的小知识! 欢迎关注、点赞 持续更新!
第一章
synchronized
Java中Synchronized的用法
synchronized是Java中的关键字,是一种同步锁。
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
一个线程访问一个对象的同步代码块时,别的线程可以访问该对象的非同步代码块而不受阻塞。
synchronized关键字不能继承。
在定义接口方法时不能使用synchronized关键字
public class ThreadTest {
public static void main(String[] args) {
System.out.println("使用关键字静态synchronized");
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
}
}
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public static synchronized void method() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public synchronized void run() {
method();
}
}
线程通知与等待
当一个线程调用一个公共变量的wait()时,该调用的线程会被阻塞挂起。
调用 wait() 方法的线程需要事先获取该对象的监视器锁。否则会抛出异常。
该调用线程会被阻塞挂起时只有通过以下情况才能返回
线程调用了该共享对象 notify()或者 notifyAll(0方法
其他线程调用了该线程 interrupt() 方法,该线程抛出 nterruptedException 异常返回。
sleep()调用线程会暂时让出指定时间的**执行权**,也就是在这期间不参与CPU的调度,但是程所拥有的监视器源,比如**锁**还是持有**不让出**的。获取到 CPU 资源后就可以继续运行了。
yield()一般很少使用这个方法,在调试或者测试时这个方法或许可以帮助复现由于并发竞争条件导致的问题,其在设计并发控制时或许会有用途。
sleep方法与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。
而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
线程中断
Java 中的线程中断是种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是根据被中断的线程根据中断状态自行处理。
interrupt() : 设置线程中断标志为true
isinterrupted():检测调用线程是否被中断 不会清除中断标志
interrupted(): 检测当前线程是否被中断 会清除中断标志 静态方法
@Override
public void run() {
//如果当前线程被中断则退出循环 则退出线程
while (!Thread.currentThread().isInterrupted() && count < 10) {
method();
}
}
当一个线程阻塞时间过长时,我们可以通过设置中断强制抛出异常而返回,使线程重新进入激活状态。
例如:休眠3s 但是发现3s内就能满足条件。如果一直等待3s就会浪费时间。调用中断强制激活。
守护线程与用户线程
Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。
JVM启动会调用main函数,其所在的钱程就是一个用户线程,其实在 JVM内部同时还启动了好多守护线程,比如垃圾回收线程。
区别:当最后一个非护线程结束时JVM正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM 退出。
当Main线程运行结束后,JVM会启动一个叫做DestroyJava VM的线程,该线程会等待所有用户线程结束后终止JVM进程。
ThreadLocal
ThreadLocal JDK 包提供的,它提供了线程本地变量,也就是如果你创建了ThreadLocal ,那么访问这个变量的每个线程都会有这个变量的一个本地副本,当多个线程操作这个变量时,实际操作的是自己本地内存里面变量,从而避免了线程安全问题。
实现原理
每次操作的都是线程内部的ThreadLocalMap,每个线程的本地变量不是存放在 ThreadLocal 里面,而是存放在调用线程的threadLocals变量里,Thr adLocal 就是个工具壳。
key 为我们定义的ThreadLocal变量this引用, value则为使用set方法设置的值。
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
// 将当前线程作为 key ,去查找对应的线手呈交量,找到则设置
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
// 没有第一次创建对应MAP
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
第二章
什么是多线程并发编程
并发是指同一个时间段内多个任务同时都在执行。
并行是说在单位时间内多个任务同时在执行。
synchronized
synchronized 块是 Java 提供的一种原子性内置锁,当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作。会带来线程调度开销。
进入synchronized块的内存语义是把在 synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变时就不会从线程的工作内存中获取,而是直接从主内存中获取。
退出synchronized块的内存语义是把在synchronized块内对共享变量修改刷新到主内存。
volatile
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存当其它线程读取该共享变量,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
什么时候使用?
写入变量不依赖变量的当前值时,因为如果依赖当前值,将是获取、计算、写入三步操作
这三步操作不是原子性的,而 volatile 不保证原子性。例如(count++ 读取count的值到工作内存,计算count值,再将count刷新进主内存。)
Unsafe
jdk提供的不安全的功能,更低层(c语言),因为是通过可以直接调用内存不直接提供给用户使用。可以进行数组边界判断与比较与交换CAS操作进行赋值。官方不推荐使用。
想要使用只能使用反射获取变量
其getUnsafe()
代码要求必须通过BootStrapClassLoader
加载,而我们使用main函数调用会使用AppClassLoader
调用得所以无法获取
@CallerSensitive
public static Unsafe getUnsafe() {
Class> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
示例
public class UnsafeTest {
static Unsafe unsafe;
private static long countOffset;
private int count = 1;
static {
//使用反射获取Unsafe 的成员交量thUnsafe
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 给权限防止禁止反射使用
field.setAccessible(true);
unsafe= (Unsafe) field.get(null);
// 设置count参数偏移量
countOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("count"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
UnsafeTest unsafeTest = new UnsafeTest();
int andAddInt = unsafe.getAndAddInt(unsafeTest, countOffset, 2);
// 原值
System.out.println(andAddInt);
// 新值
System.out.println(unsafeTest.count);
}
}
乐观锁与悲观锁
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。
乐观锁 相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而 在进行数据提交更新时,才会正式对数据冲突与否进行检测 。
独占锁与共享锁
独占锁保证任何时候都只有一个线程能得到锁, ReentrantLock 就是以独占方式实现共享锁则可以同时由多个线程持有 ,例如 ReadWriteLock 锁,它允许一个资源可以被多线程同时进行读操作。
独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性 ,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。
共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。
自旋锁
当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。
当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程,而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。
自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取默认次数是 10,很有可能在后面几次尝试中其他线程己经释放了锁,如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU 时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费。所以选择获取锁释放锁快的。