1.同步处理
同步处理:指的是同步所有线程,保证同一时间的线程不是一起进入到方法或者代码块,必须得一个一个来。
package com.wschase.thread2;
/**
* Author:WSChase
* Created:2019/1/9
*/
class MyThread implements Runnable {
private int ticket = 10 ; // 一共十张票
@Override public void run() {
while (this.ticket > 0) { // 还有票
try {
Thread.sleep(200);
} catch (InterruptedException e) {
//TODO Auto-generated catch block
e.printStackTrace();
} // 模拟网络延迟
System.out.println(Thread.currentThread().getName()+",还有" +this.ticket -- +" 张票");
}
}
}
public class MyRunnable {
public static void main(String[] args) {
MyThread mt = new MyThread() ;
new Thread(mt,"黄牛A").start();
new Thread(mt,"黄牛B").start();
new Thread(mt,"黄牛C").start();
}
}
从上面的例子我们可以看出来,这是一个线程不同的例子,它会同时出现多个用户抢票。
(1)synchronized处理同步问题
如果想要实现这把锁的功能,那么就可以采用synconized关键字。它的处理有两种模式:同步代码块、同步方法。
使用同步代码块方式:如果想对代码的局部做同步,就用我们的局部块。
package com.wschase.thread2;
/**
* Author:WSChase
* Created:2019/1/9
*/
class MyThread implements Runnable {
private int ticket = 10; // 一共十张票
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//在同一时间,只允许一个线程进入代码块
System.out.println("----");//如果没有这一句话,一旦有一个进入到循环里面占用了资源那么就会一直在里面
//但是加上这一句话以后就可以同时有多个线程进入到这个循环里面,所以运行的结果
//才是有A,有B,有C。
synchronized (this) {
if (this.ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
//TODO Auto-generated catch block
e.printStackTrace();
} // 模拟网络延迟
System.out.println(Thread.currentThread().getName() + ",还有" + this.ticket-- + " 张票");
}
}
}
}
}
public class MyRunnable {
public static void main(String[] args) {
MyThread mt = new MyThread() ;
new Thread(mt,"黄牛A").start();
new Thread(mt,"黄牛B").start();
new Thread(mt,"黄牛C").start();
}
}
使用局部方法方式:如果我们想把一个方法里面所有的逻辑给它做同步,那么我们就使用同步方法。
package com.wschase.thread2;
/**
* Author:WSChase
* Created:2019/1/9
*/
class MyThread implements Runnable {
private int ticket = 10; // 一共十张票
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("---");
this.sale();
}
}
public synchronized void sale() {
if (this.ticket > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
//TODO Auto-generated catch block
e.printStackTrace();
} // 模拟网络延迟
System.out.println(Thread.currentThread().getName() + ",还有" + this.ticket-- + " 张票");
}
}
}
public class MyRunnable {
public static void main(String[] args) {
MyThread mt = new MyThread() ;
new Thread(mt,"黄牛A").start();
new Thread(mt,"黄牛B").start();
new Thread(mt,"黄牛C").start();
}
}
对于以上两种方法就是线程同步处理的两种方法,从上面的方法我们可以看到,在循环里面的线程同步处理,我们一定要注意,如果循环里的第一句话就用了synchnized修饰了的话,那么只要有一个线程进入循环,将会一直占用资源直到循环停止。
2.关于synchronized的额外说明
实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行一个对象的同步代码块。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁住的是对象本身也就是this。
3.锁住代码块
全局锁:synchronized(class)
这个的意思是锁住这个类,只要是属于这个类的对象都会被锁住。全局锁是有风险的。
对象锁:synchronzied(this/obj)
对象锁锁住的只是这个对象,它的范围更小一些。
4.synchronized是怎么实现的
执行同步代码块以后首先需要执行的指令是:monitorenter,退出的时候执行指令:monitorexit。通过分析以后我们可以看到,使用synchornized进行同步的关键在于必须要对对象的监视器monitor进行获取,当线程获取但监视器monitor以后才可以继续往下执行代码,否则之恩等待。而这个获取的过程是相互排斥的,即同一时间只能有一个线程获取监视器monitor。
5.显示锁
package com.wschase.suo;
import java.time.LocalDateTime;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**线程的同步与死锁
* Author:WSChase
* Created:2019/1/10
*/
public class Testsuo {
//对于同步与死锁我们只需要掌握概念就可以了
public static void main(String[] args) {
LockRunnable runnable=new LockRunnable();
Thread thread1=new Thread(runnable,"黄牛A");
Thread thread2=new Thread(runnable,"黄牛B");
Thread thread3=new Thread(runnable,"黄牛C");
thread1.start();
thread2.start();
thread3.start();
}
}
class LockRunnable implements Runnable {
private int tick = 10;
private Lock lock = new ReentrantLock();
@Override
public void run() {
//我们把加锁放在try之前,把解锁放在fainally里面,这样可以保证我们的锁一定可以加锁和解锁成功
//这样才可以达到我们想要的效果:就是一个线程不会长期的占用一把锁
for (int i = 0; i < 10; i++) {
System.out.println("-----");//不要让循环和锁紧挨着
lock.lock();//加锁
try {
if (this.tick > 0) {
System.out.println(Thread.currentThread().getName() + "剩余票数" + --this.tick);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//多线程没有办法同时访问这块代码
lock.unlock();//解锁
}
}
}
}
通过Lock,显示加锁,我们可以先加在哪就在哪。
如果我们把sleep()放在循环紧跟的第一句话,那么我们就不需要打印-----了,这样也可以保证出来的结果A、B、C都会出现。
注意:虽然Lock可以自由的使用,显示的使用,但是我们的synchronized是隐式的但是经过不断的优化以后,我们的synchronized已经变得更好了,所以我们还是优先使用synchronized关键字实现线程同步加锁。
对于synchronized我们掌握下面这一张表就可以啦:
6.synchronized优化
(1)CAS(竞争锁)
我们使用CAS也是来给线程进行加锁和解锁的。使用锁的时候,线程获取锁是一种悲观策略,悲观策略:当前线程进去以后其他线程就不可以进去了,这样其他线程就会产生阻塞了。而CAS操作(又称为无锁操作)也是一种乐观锁策略,它就阿佘所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然就不会阻塞其他线程的操作了。那么,如果出现了冲突该怎么办?无锁操作时使用CAS compare and swap(比较转换)来鉴别线程是否出现冲突的,如果出现了冲突就重试当前操作直到没有冲突为止。
7.锁的几种状态
记住:
锁一共有4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(他们的优先级是逐渐升级的,还有锁是不能降级的,它的目的是为了提高获得锁和释放锁的效率)。
(1)偏向锁
这是最乐观的一种锁:从始至终只有一个线程请求一把锁。
偏向锁的获取:分为两个过程
首先判断当前线程是不是偏向锁,如果测试成功了,那么它已经获得了偏向锁了;如果不是,我们再来看这个对象是否有标志位表示偏向锁的标志位,然后再进行测试,这时候如果测试出来的标志位为1表示当前锁是偏向锁,如果测试出来是0,则使用CAS竞争锁;如果已经设置了竞争锁,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销:
等到竞争出现才释放锁的机制;所以当其他线程竞争偏向锁的时候,持有偏向锁的线程才会释放锁。
(2)轻量锁
多线程在不同时段请求一把锁,也就是说没有锁竞争。只有在同一个之间段多线程访问才存在竞争。
加锁:在每次获取的时候都需要先创建栈帧,把对象头拷贝到空间里面来,然后我再用CAS去做竞争,如果成功了我就获取到当前的锁了,如果失败了,说明有人和我在竞争,我就额人家竞争。
解锁:
使用原子的CAS操作将轻量锁替换回对象头(Mark world),但是替换不一定能成功,因为有可能会有其他线程在使用这个对象头,所以如果替换成功了则表示没有竞争,如果不成功,则表明有竞争,有竞争以后还得想办法释放,这时候就将锁编程重量级的锁——锁膨胀,这就回到synchronized。升级完重量级锁以后,一旦我们的锁已释放,就会进行新一轮的竞争。
synchronized(重量级锁的特点):多线程在同一时间竞争一个资源,谁竞争成功以后就会造成其他线程阻塞。
(3)重量级锁
它是JVM种最为基础的锁实现 。在重量级锁中,JVM会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
java线程中的阻塞和唤醒都依赖于我们的操作系统。
为了避免这样的阻塞以及唤醒状态,我们有一个自旋状态,通过自旋来减轻阻塞以及唤醒的状态,减轻操作系统不停的转换。
总结:(对锁的掌握就这三句话)
(1)重量级锁会引起阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自旋,来避免线程在面对非常小的synchronized代码块,仍会被阻塞、唤醒的情况。
(2)轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
(3)偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的时锁仅会被同一线程持有的情况。
8.锁的其他优化
(1)锁粗化
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。
(2)锁清楚
当我们在一个方法里面执行一段代码的时候,只要这段代码一直在这段代码的作用域里面,我们就可以不需要考虑使用线程安全。因为它始终时属于这个代码块的,多线程是访问不到这个代码块的。
9.死锁
同步的本质在于:一个线程得等到另一个线程执行完毕以后擦可以执行,并且如果现在相关的几个线程彼此之间都在等待,那么就会造成死锁。
死锁的例子:相互去竞争资源
package com.wschase.suo;
/**死锁
* Author:WSChase
* Created:2019/1/10
*/
public class TestDeadLock {
//资源笔
private static Pen pen=new Pen();
//资源书
private static Book book=new Book();
public static void main(String[] args) {
//线程1
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
synchronized (book){
System.out.println(Thread.currentThread().getName()+"我有书,但是还在用");
synchronized (pen){
System.out.println(Thread.currentThread().getName()+"想做笔记,但是没有笔,把你的给我");
}
}
}
},"Thread-A");
//线程2
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
synchronized (pen){
System.out.println(Thread.currentThread().getName()+"我有笔,但是还在用");
synchronized (book){
System.out.println(Thread.currentThread().getName()+"想看书,但是没有书,把你的给我");
}
}
}
},"Thread-B");
thread1.start();
thread2.start();
}
}
class Pen{
private String name="笔";
public String getName() {
return name;
}
}
class Book{
private String name="书";
public String getName() {
return name;
}
}
10.ThreadLocal
(1)概念
ThreadLocal用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其他线程。它于同步机制正好是相反了,同步时保证多线程访问数据的一致性。而ThreadLocal保证的时多线程变量的独立性。
package com.wschase.suo;
/**
* Author:WSChase
* Created:2019/1/10
*/
public class TestThreadLocal {
//多线程共享
private static String staticCommValue;
//多线程独立
private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
//main方法是在Thread - main 线程中执行
//1.主线程中修改staticCommValue
staticCommValue = "这是main线程修改的值";
//2.在主线程中修改threadLocal
threadLocal.set("这是main线程修改的threadLocal的值");
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
//3.子线程中修改staticCommValue
staticCommValue = "这是子线程修改的值";
//4.在子线程中修改threadLocal
threadLocal.set("这是子线程修改的threadLocal的值");
System.out.println("子线程"+threadLocal.get());
}
},"子线程");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印输出
System.out.println(staticCommValue);
System.out.println(threadLocal.get());
}
}
这个时候我们的资源是共享的。