在编程领域,我们要想有所提高,那么需要跨越的一座大山就是多线程与高并发。这是我多线程与高并发学习系列文章的第一篇,我会从最基本的什么是线程讲起,希望更多的人能够从这些文章中有所收获为你学习多线程与高并发助力,用最少的时间得到最大的成果。
线程
基本概念
说到线程就得从程序说起,程序是躺在磁盘中的应用代码,进程是运行中的程序,而线程是进程的不同执行路径,用专业一点的说法就是进程是资源分配的基本单位,而线程是调度执行的基本单位。
线程的创建与使用
线程的实现方式
1.继承Thread 重写run()方法
2.实现Runnable接口,重写run()方法
3.线程池
线程的几种状态
new runnable terminated waiting timeWaiting blocked
常用法法
Thread.sleep(time) 线程进入睡眠 睡眠时间结束后继续运行
Thread.yield() 放弃当前的执行机会进入就绪队列 等待下次运行
t1.join() 在当前线程中调用其他线程的join方法 这样会让当前线程等待其他线程执行完毕后再执行当前线程,调用自己的join无效
synchronized的使用(可保证原子性和有序性)(锁的是对象)
1.两种使用方式 第一 用于方法上,当关键字修饰方法即可 第二用synchronized(){ } 修饰的代码块,二者没有很大区别,只是同步的代码块大小不同而已。
2.synchronized加锁的对象可以是任意的,一般来说方法前加关键字修饰时所对象是this。静态方法或代码块为class对象。
public class T {
private static int count = 10;
public synchronized static void m() { //这里等同于synchronized(FineCoarseLock.class)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized(T.class) { //考虑一下这里写synchronized(this)是否可以?
count --;
}
}
}
3.synchronized加锁的过程中有锁升级的过程
JDK早期是底层实现是重量级锁 后来经过改造加锁的过程为 偏向锁 -> 自旋锁(有个自旋次数) -> 重量级锁。
4.synchronized锁是可重入锁 。
5.锁定方法和非锁定方法可同时执行。
6.方法内发生异常时会释放锁。
7.什么时候用自旋锁 什么时候用重量级锁?
锁的代码执行时间短,线程比较少的时候用自旋
锁的代码执行时间长 线程比较多的时候用重量级锁
8.应避免锁对象发生变化(对象属性变了不影响,引用发生变化就会有影响)
9.synchronized锁的实现是:锁对象头的前两位用来做锁的实现,两位有四种状态,分别代表无锁、偏向锁、轻量级锁、重量级锁
*注:银行问题上存款取款都得加synchronized *
/**
* 面试题:模拟银行账户
* 对业务写方法加锁
* 对业务读方法不加锁
* 这样行不行?
*
* 容易产生脏读问题(dirtyRead)
*/
import java.util.concurrent.TimeUnit;
public class Account {
String name;
double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public /*synchronized*/ double getBalance(String name) {
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}
锁
一般来说有两大类 乐观锁 悲观锁。
偏向锁:
乐观锁,加锁只是标记下当前线程号
自旋锁:
乐观锁 ,不释放CPU,等待
重量级锁:
悲观锁,需要OS来协助。
volatile(保证可见性和禁止指令重排序)
可见性
一个线程的修改对其他线程可见。线程之间具有隔离性,多线程参与访问同一个对象时,可能会有问题,而用volatile修饰的对象,当一个线程对该对象做了修改后,其他线程也可见。
指令重排序
用volatile修饰的变量,在操作时禁止指令重排序 如用volatile修饰的对象在创建时指令不能重排序
注:创建对象时有3步 1.分配内存空间 2.初始化 3.给引用赋值 3条指令在执行过程中可能会进行指令重排序
例如DCL单例问题(double check lock)
指令重排序使得引用的值过早的给赋值,使得对象还未初始化就使得引用有值,因此会使得其他线程获取到未初始化的对象,影响使用。
volatile不能保证原子性,不能代替synchronized
CAS(无锁优化)
compare and set ,自旋锁、乐观锁,底层使用原子操作(CPU指令级操作)。JUC中的AutomicXX等原子类的底层实现都是利用的CAS。
CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
一个线程去修改某个值的过程可简单描述为这样:参数1 当前线程现在读取到的真实内存值(新读取的值) 参数2 当前线程在加载程序进入线程空间时获取到的值(旧值)参数3 要赋的值 参数1和参数2比较 如果相等修改 否则失败
计数增长问题
在大多数的程序中都会涉及到计数问题,而为了确保数字的准确性,我们有如下几种方式来确保程序的健壮性。
1.synchronized
2.AutomicXXX原子类
3.LongAdder类 JUC中的一个类,底层采用分段锁
static LongAdder count = new LongAdder();
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[500];
for(int i=0; i<threads.length; i++) {
threads[i] =
new Thread(()-> {
for(int k=0; k<100000; k++) count.increment();
});
}
}