加锁
synchronized
1.修饰方法(普通方法,静态方法)普通方法实际上加到了this上,静态方法加到了类对象上。
2.修饰代码块 手动指定加到那个对象上
明确锁对象针对那个对象加锁,如果两个线程针对同一个对象加锁,就会出现锁竞争,一个线程先能获取到锁,另一个线程阻塞等待,等待上一个线程解锁,它才能获取锁成功
如果两个线程针对不同对象加锁,就不会产生锁竞争,这两个线程都能获取到各自的锁
如果两个线程,一个线程加锁,另一个线程不加锁,这个时候不会有锁竞争
synchronized修饰方法
class flg
{
public static int m=0;
public synchronized void add1()
{
m++;
}
}
synchronized修饰代码块
class flg
{
public static int m=0;
public void add2()
{
synchronized (this)
{
m++;
}
}
}
这两种写法本质上是一样的。
一个加锁另一个不加锁的情况
class flg
{
public static int m=0;
public void add2()
{
synchronized (this)
{
m++;
}
}
public void add()
{
m++;
}
}
当两个线程分别去调用这两个方法是,实际上相当于没加锁
synchronized的力量是jvm提供的,jvm的力量是操作系统提供的,操作系统的力量是CPU提供的,从根本上说,是CPU提供了这样的指令才能让操作系统的API提供给JVM,JVM提供给synchroniz
d。
3.可重入
一个线程针对同一个对象连续连续加锁两次,如果没问题,就叫可重入,如果有问题就叫不可重入
例如这段代码
public synchronized void add2()
{
synchronized (this)
{
a++;
}
}
锁对象是this,只要有线程调用add,进入add方法的时候就会先加锁(能够加锁成功),紧接着又遇到了代码块,再次尝试加锁,这两个线程是同一个线程,如果允许这个操作,这个锁是可重入的,如果不允许这个操作(第二次加锁会阻塞等待),就是不可重入的,这个情况会导致死锁
,java把synchronized设定成可重入的了。
java标准库中的线程安全类
如果多个线程操作同一个集合类,就要考虑到线程安全的问题
Arraylist 、Linkedlist 、HashMap 、TreeMap、HashSet、TreeSet、StringBuilder 这些类在多线程代码中要格外注意
Vector、HashTable、ConcurrentHashMap、StrringBuffer 已经内置synchronized加锁,相对来说更安全一点
死锁
1.一个线程,一把锁,连续加锁两次,如果锁是不可重入锁,就会死锁
2.两个线程两把锁,t1和t2各自针对锁A和锁B加锁,再次尝试获取对方的锁
举个例子小明有一个羽毛球,它对这个羽毛球加锁了,小张有一双羽毛球拍,并对它加锁,小明说小张把你的羽毛球拍借给我用几天,而小张对小明说,你把你的羽毛球借我用几天,这不就僵住了吗?
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
synchronized (qiu)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (pai)
{
System.out.println("小明把球和球拍都拿到了");
}
}
}
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
synchronized (pai)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (qiu)
{
System.out.println("小张把球和球拍都拿到了");
}
}
}
});
我们执行这个程序的时候,会发现程序僵住了。
我们借助jconsole这样的工具来进行定位,看线程的状态和调用栈,分析代码在哪里死锁。
我们看到线程1此时是阻塞状态,而阻塞发生在17行
我们再来看一下线程2
此时线程2也是阻塞状态,阻塞发生在38行
这也就反映了当两个线程分对锁A、锁B加锁时,再尝试获取对方的锁时,会死锁。
3.多个线程多把锁
像这里有5个人桌子上是5双筷子,每个人都拿左边的筷子,并对其加锁,我们会发现那个人都吃不了饭,每个人都只有一双筷子。
死锁的必要条件:
1.互斥使用:
线程1拿到锁之后,线程2就得等着。
2.不可占用:
线程1拿到锁之后,必须是线程1主动释放,不能说线程2给强行获取到。
3.请求和保持:
线程1拿到锁A之后,再尝试获取锁B,A这把锁还是保持的(不会因为获取锁B,就把A给释放了)
4.循环等待:
线程1尝试获取到锁A和锁B 线程2尝试获取到锁B和锁A.线程1在获取B的时候等待线程2释放B,线程2在获取A的时候等待线程1释放A.
前三个条件是锁的基本特性,循环等待是这四个条件里唯一一个和代码结构相关的,也是我们可以控制的。
如何打破死锁呢?突破口就是循环等待
办法:给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁,让线程遵守这个顺序,此时循环等待自然破除。
我们给筷子编了个号,然后让每个人都拿两边小的那一个, 然后我们发现e此时就可以拿到1和5这个筷子,那么它就可以吃饭了,当它吃完的时候,1和5放下,此时d也可以拿起5、4吃饭了,然后依次类推,每个人都能吃上饭,这样就破除了死锁。
明白了如何破除死锁我们再来看一下球和球拍那个代码,如果此时我们给这两个线程加锁固定个顺序,先加qiu,再加pai
public static void main(String[] args) {
Object qiu=new Object();
Object pai=new Object();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
synchronized (qiu)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (pai)
{
System.out.println("小明把球和球拍都拿到了");
}
}
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
synchronized (qiu)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (pai)
{
System.out.println("小张把球和球拍都拿到了");
}
}
}
});
thread1.start();
thread2.start();
}
我们看一下运行结果:
死锁的情况也就破除了。
内存可见性问题:
这个情况就是内存可见性问题,这也是一个线程不安全问题,一个线程读,一个线程改。
这里使用汇编来理解,大概就是这两操作,1.load ,把内存中flag的值,读取到寄存器里 2.cmp 把寄存器中的值,和0进行比较,根据比较结果,决定下一步往那个地方执行。由于load执行速度太慢(相当于cmp)来说,再加上反复load的结果都一样,编译器进行了优化,不再真正的重复load了,判定好像没有人改flag值,干脆只读取一次就好。
内存可见性问题:
一个线程针对一个变量进行读取操作,同时另一个线程对这个变量进行修改,此时读取到的值,不一定是修改之后的值,这个读线程没有感知到变量的变化,归根到底是编译器/jvm在多线程环境下优化时产生了误判。
此时我们给flag这个变量加上volatile关键字,意思就是告诉编译器,这个变量是"易变的",每次都要重新读取这个变量的内存内容,不要就行优化。
上述所说的内存可见性 编译器优化问题,也不是始终会出现的(编译器可能存在误判,但也不是100%就误判)
class MyCounter{
public volatile int flag=0;
}
public class Mycount {
public static void main(String[] args) {
MyCounter myCounter=new MyCounter();
Thread t1=new Thread(() ->{
while(myCounter.flag==0)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1循环结束");
});
Thread t2=new Thread(() ->{
Scanner scanner =new Scanner(System.in);
System.out.println("请输入一个整数");
myCounter.flag=scanner.nextInt();
});
t1.start();
t2.start();
}
}
像这段代码,如果我们在while循环里面加上个sleep,此时编译器就没有优化,while循环会终止,
从JMM角度重新表述内存可见性问题: java程序里有主内存,每个线程还有自己的工作内存,t1线程进行读取的时候,只是读取了工作内存的值,t2线程进行修改的时候,先修改工作内存的值,然后把工作内存的内容同步到主内存中,但是由于编译器优化,导致t1没有重新从主内存同步数据到工作内存,读到的结果就是修改之前的结果,如果把主内存代替成咱们说的"内存" 把工作内存代替成”CPU"寄存器,工作内存,不只含有CPU的寄存器,还可能有CPU的缓存cache。
CPU读取寄存器,速度比读取内存快很多,因此会在CPU内部引入缓存cache 寄存器存储空间小,读写速度快,但是价格贵,中间搞了个cache,存储空间居中,读写速度居中,成本居中,内存存储空间大,读写速度慢,便宜。当CPU要读取一个内存数据时,可能直接读取内存,也可能是读cache,还可能是读取寄存器。
volatile不保证原子性,原子性是靠synchronized来保证的,synchronized和volatile都能保证线程安全,但是不能使用volatile处理两个线程并发++这样的问题。
wait 和notify
线程最大的问题就是抢占式执行,随机调度,于是我们发明了一些东西来控制线程之间的执行顺序,虽然线程在内核里的调度是随机的,但是可以通过一些cpi,让线程主动阻塞,主动放弃CPU,比如t1t2俩线程,希望t1先干活,干的差不多了,再让t2来干活,就可以让t2先wait(阻塞,主动放cpu),等t1干的差不多了,再通过notify通知t2,把t2唤醒,让t2接着干。
上述场景,使用join或者sleep行不行呢?
使用join,则必须让t1彻底执行完,t2才能运行,如果是希望t1先干%50的活,就让t2开始行动,join也无能为力,使用sleep,指定一个休眠时间,但是t1执行这些活,到底需要多少时间,不好估计。
wait进行阻塞,某个线程调用wait方法,就会进入阻塞,此时就处在WAITING,object.wait(),wait不加任何参数就是死等,一直等到其他线程唤醒它。wait加参数,指定了等待的最大时间
wait的带有等待时间的版本,看起来和sleep有点像,其实还是有本质区别的,虽然都能指定等待时间,虽然也能被提前唤醒(wait时使用notify唤醒,sleep是使用interrupt唤醒),但是notify唤醒wait
不会有异常,interrupt唤醒sleep则是出异常了。
wait,notify,notifyall 这几个方法,都是Object类的方法。
wait sleep区别总结:
1.相同点:都是使线程暂停一段时间
2.wait 是Object类的方法,而sleep是Thread类的方法
3.wait必须在synorchnoized修饰的代码块或方法里使用,而sleep在哪都可以
4.调用wait,线程进行BLOCK状态,调用wait线程会主动释放锁,而线程调用sleep会处于TIMED_WAIT状态,不涉及锁操作.
public static void main(String[] args) {
Object lock=new Object();
Thread t1=new Thread(() -> {
int i=0;
for(i=0;i<5;i++)
{
;
}
System.out.println("线程1已经执行完,去通知线程2执行");
synchronized (lock) {
lock.notify();
}
});
Thread t2=new Thread(() ->{
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程2已经执行完");
});
t2.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.start();
}
}
像这段代码,我们在t2里面调用lock.wait()就是让t2线程等待t1让它先干完活,虽然这里阻塞了,阻塞在synchronized代码块里,实际上这里的阻塞是释放了锁的,此时其他线程是可以获取到object这个对象的锁的,此时这里的阻塞,就出在WAITING状态, 当t1干完活后,再调用lock.notify()通知线程2干活,线程2此时重新获取到锁。
我们要注意,启动线程的时候,先让t2先启动,过段时间再让t1先干活,因为如果先启动t1,可能会存在t1线程已经执行完了,而t2线程此时再执行,此时线程2就会一直阻塞下去,t1的notify已经执行完了,也就不起作用了。
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
像这样线程2就会一直阻塞下去,没法被唤醒。
这里面还需要注意的一个点是wait(),notify()这两个方法需要搭配synchronized使用,为啥呢?
因为wait操作,先释放锁,进行阻塞等待,收到通知以后,尝试获取锁,并且在获取锁之后,继续往下执行。
notify和notifyAll区别
多个线程wait的时候,notify随机唤醒一个,notifyAll 所有线程都唤醒,这些线程在一起竞争锁。
三个线程分别只能打印ABC,且保证按照ABC的顺序打印。
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
System.out.println("A");
synchronized (locker1) {
locker1.notify();//通知线程2,唤醒线程2
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
try {
locker1.wait();//等待线程1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
synchronized (locker2) {
locker2.notify();//通知线程3唤醒线程3
}
});
Thread t3 = new Thread(() -> {
synchronized (locker2) {
try {
locker2.wait();//等待线程2
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
});
t2.start();//先让t2 t3先启动,防止线程1的notify提前被调用,线程2就无法被唤醒
t3.start();
Thread.sleep(100);
t1.start();
}
单例模式:单个实例(对象)
在有些场景中,有的特定的类,只能创建一个实例类,不应该创建多个实例,java里实现单例模式有很多种,我们主要介绍两种:(1)饿汉模式 (2)懒汉模式
(1)饿汉模式
public class Signale {
//在此处,先把这个实例创建出来
public static Signale signale=new Signale();
//如果需要使用这个唯一实例,统一通过Singale.getInstance()
public static Signale getInstance()
{
return signale;
}
//把构造方法设为Private,在类外面,就无法通过new的方式来创建Singale实例了。
private Signale()
{
}
}
像这里static修饰这个对象,就会保证这个实例是唯一的,保证这个实例在一定时机被创建起来 。
我们用private这个属性,在类外面就无法通过new的方式来创建这个Singale实例了。
类对象本身和static没关系,而是类里面使用static修饰的成员会作为类属性,也就相当于这个属性对应的内存空间在类对象里面
class text123
{
public int a;
public static int b;
}
public class Thread5 {
public static void main(String[] args) {
text123 t1=new text123();
text123 t2=new text123();
//两个实例分别指向两份不同的a
t1.a=20;
t2.a=30;
//被static 修饰的b只有一份,两次都指向同一个b
text123.b=10;
text123.b=20;
System.out.println("t1="+t1.a);
System.out.println("t2="+t2.a);
System.out.println(text123.b);
}
}
类加载:运行一个java程序,就需要让java进程能够找到并读取对应的.class文件就会读取文件内容,并解析并构成类对象,这一系列的过程操作,叫做类加载。
单例模式的懒汉模式实现:
public class Signallazy {
public static Signallazy signallazy=new Signallazy();
public Signallazy getInstance()
{
if(signallazy==null)
{
signallazy=new Signallazy();
}
return signallazy;
}
private Signallazy()
{
}
}
我们再把饿汉模式拿过来看一下:
public class Signale {
//在此处,先把这个实例创建出来
public static Signale signale=new Signale();
//如果需要使用这个唯一实例,统一通过Singale.getInstance()
public static Signale getInstance()
{
return signale;
}
//把构造方法设为Private,在类外面,就无法通过new的方式来创建Singale实例了。
private Signale()
{
}
}
这两个模式哪个是安全的呢?
像饿汉模式我们知道它只涉及读,不涉及修改,那么它应该是安全的。
而懒汉模式我们发现它涉及读和修改两种操作,如果不给它加锁,它是不安全的。
如果是一个线程,那么是安全的,但是如果是多个线程,像这里的t2线程就会读到“脏数据",也就是未修改后的值。
那么我们应该加上锁
public class Signallazy {
public static Signallazy signallazy=new Signallazy();
public Signallazy getInstance()
{
synchronized (Signallazy.class) {
if (signallazy == null) {
signallazy = new Signallazy();
}
}
return signallazy;
}
private Signallazy()
{
}
}
那这样是不是就完美了呢?我们知道加锁操作是有开销的 ,当signallazy一旦不为空时,此是会直接返回signallazy,相当于一个是比较操作,一个是返回操作,这两个都是读操作,而不涉及修改操作,此时就不需要加锁了,因此我们在外边在判断一下signallazy是否为空,是否需要加锁就行
public class Signallazy {
public static Signallazy signallazy = new Signallazy();
public Signallazy getInstance() {
if (signallazy == null) { //第一个if用来判断是否需要加锁,
synchronized (Signallazy.class) {
if (signallazy == null) {//第二个if用来判断是否需要new对象
signallazy = new Signallazy();
}
}
}
return signallazy;
}
private Signallazy()
{
}
}
第一个if语句用来判断是否需要加锁,第二个if语句用来判断是否 需要new对象。
那么代码这样写是不是就安全了吗?我们明白只有第一次读才是读的内存,后面读的都是寄存器和cache,内存可见性问题,另外还会涉及到指令重排序问题。
指令重排序问题:本质上是编译器优化出了问题。
signallazy=new Signallazy() 拆分成三个步骤:1.申请内存空间 2.在内存空间里构造合法的对象
3.把内存空间的地址赋值给引用signallazy.
如果编译器为了提高效率,调整代码顺序,出现指令重排序的问题正常顺序是1、2、3,这时可能为1、3、2如果是单线程,2和3顺序颠倒不会出现问题,但是如果是多线程,假设线程1按照1、3、2执行,当执行完3,执行2之前,线程1被切除CPU,线程2执行,在线程2看起来此处引用非空,就直接返回了,但是由于t1还没执行2操作,此时t2拿到的是一个非法对象,还没构造完成的不完全对象。
针对内存可见性和指令重排序问题,我们需要用volatile!!!!
public volatile static Signallazy signallazy = new Signallazy();
用volatile修饰一下signallazy就可以了。
下边这就是单例模式懒汉模式的安全版本了。
public class Signallazy {
public volatile static Signallazy signallazy = new Signallazy();
public Signallazy getInstance() {
if (signallazy == null) { //第一个if用来判断是否需要加锁,
synchronized (Signallazy.class) {
if (signallazy == null) {//第二个if用来判断是否需要new对象
signallazy = new Signallazy();
}
}
}
return signallazy;
}
private Signallazy()
{
}
}