常用的synchronized锁
对象锁和类锁的区别‘
对象锁:一般直接用synchronized修饰某个方法即可
类锁:一般是修饰static的方法,相当于是修饰整个类,所以称类锁
总结:如果是同一个对象调用对象锁的方法时候,他们会按照顺序去执行方法,如果是不同的对象调用对象锁的方法,他们会异步去执行方法。
如果是类锁,不管你是否是同一个对象,只要是方法属于类锁,他们都会按照顺序去执行方法。
代码演示类锁和对象锁
以后通用的线程睡眠工具类
public class SleepTools {
//按秒休眠
public static final void sencod(int s){
try {
TimeUnit.SECONDS.sleep(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//毫秒休眠
public static final void ms (int ms){
try {
TimeUnit.MICROSECONDS.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建一个SycnzAndIns类。有以下两个方法
public class SycnzAndInst {
//该方法是对象锁
public synchronized void syncObj(){
System.out.println(Thread.currentThread().getName()+"-----执行中---");
SleepTools.sencod(3);
}
//该方法是SycnzAndInst类锁
public synchronized static void syncClass(){
System.out.println(Thread.currentThread().getName()+"-----执行中---");
SleepTools.sencod(3);
}
}
在创建两个线程类,并且用构造方法注入SycnzAndIns对象
class InstancSyncThread1 extends Thread{
//使用构造方法注入SycnzAndInst对象
private SycnzAndInst sycnzAndInst;
public InstancSyncThread1(SycnzAndInst sycnzAndInst){
this.sycnzAndInst = sycnzAndInst;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"---开始---");
sycnzAndInst.syncObj();
SleepTools.sencod(1);
System.out.println(Thread.currentThread().getName()+"---结束---");
}
}
class InstancSyncThread2 extends Thread{
//使用构造方法注入SycnzAndInst对象
private SycnzAndInst sycnzAndInst;
public InstancSyncThread2(SycnzAndInst sycnzAndInst){
this.sycnzAndInst = sycnzAndInst;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"---开始---");
sycnzAndInst.syncObj();
SleepTools.sencod(1);
System.out.println(Thread.currentThread().getName()+"---结束---");
}
}
在SycnzAndInst类中写上main函数。先测试对象锁
public static void main(String[] args) {
//注意这里,创建同一个对象
SycnzAndInst sycnzAndInst = new SycnzAndInst();
InstancSyncThread1 syncThread1 = new InstancSyncThread1(sycnzAndInst);
syncThread1.setName("线程1:");
InstancSyncThread2 syncThread2 = new InstancSyncThread2(sycnzAndInst);
syncThread2.setName("线程2:");
//开启启动两个线程
syncThread1.start();
syncThread2.start();
}
线程1:---开始---
线程1:-----执行中---
线程2:---开始---
线程2:-----执行中---
线程1:---结束---
线程2:---结束---
1:我们可以看到锁对象的时候,这时候,两个线程,如果线程1正在调用syncObj()方法时候,那么线程2就必须等待线程1把syncObj()方法执行完后,才可以使用syncObj()方法。也就是说synchronized 锁对象时候,同一个对象调用的方法必须等待另一个调用结束才可以进行调用。
2:如果是两个不同的对象,调用加synchronized修饰的方法时候,采取的是异步执行
public static void main(String[] args) {
//注意这里,通过new SycnzAndInst()创建两个不同的的对象
InstancSyncThread1 syncThread1 = new InstancSyncThread1(new SycnzAndInst());
syncThread1.setName("线程1:");
InstancSyncThread2 syncThread2 = new InstancSyncThread2(new SycnzAndInst());
syncThread2.setName("线程2:");
//开启启动两个线程
syncThread1.start();
syncThread2.start();
}
线程1:---开始---
线程2:---开始---
线程2:-----执行中---
线程1:-----执行中---
线程2:---结束---
线程1:---结束---
从结果可以方法对象锁只对当前对象起效果,如果是不同对象调用加锁的方法,他们是采用异步执行的。
接下来看下类锁
1.不同对象调用 synchronized static 修饰的同一个方法
public static void main(String[] args) {
//注意这里,通过new SycnzAndInst()创建两个不同的的对象
InstancSyncThread1 syncThread1 = new InstancSyncThread1(new SycnzAndInst());
syncThread1.setName("线程1:");
InstancSyncThread2 syncThread2 = new InstancSyncThread2(new SycnzAndInst());
syncThread2.setName("线程2:");
//开启启动两个线程
syncThread1.start();
syncThread2.start();
}
线程1:---开始---
线程1:-----执行中---
线程2:---开始---
线程2:-----执行中---
线程1:---结束---
线程2:---结束---
2.同一个对象调用synchronized static修饰的同一个方法
public static void main(String[] args) {
SycnzAndInst sycnzAndInst = new SycnzAndInst();
InstancSyncThread1 syncThread1 = new InstancSyncThread1(sycnzAndInst);
syncThread1.setName("线程1:");
InstancSyncThread2 syncThread2 = new InstancSyncThread2(sycnzAndInst);
syncThread2.setName("线程2:");
//开启启动两个线程
syncThread1.start();
syncThread2.start();
}
线程1:---开始---
线程1:-----执行中---
线程2:---开始---
线程2:-----执行中---
线程1:---结束---
线程2:---结束---
我们发现不管是否是同一个对象调用类锁的方法时候,他们总是按照顺序执行加锁的方法的,也就是说只要是类锁的方法,不管对象是否是同一个,都必须等待上一个线程把方法执行完毕后,才可以轮到下一个线程执行方法
Volatile关键字
作用: 保证变量在内存的可见性。但是无法保证原子操作,属于线程不安全
推荐使用场景:一个线程写,多个线程读
代码演示线程不安全的情况。当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
class RunableThread implements Runnable{
private volatile int nums = 0;
@Override
public void run() {
//操作系统的运算符不是原子操做,一个+操作,虚拟机可能要执行好几条指令才能完成+操作
nums= nums+10;
SleepTools.ms(100);
nums= nums+10;
System.out.println(Thread.currentThread().getName()+"--nums="+nums);
}
}
/**
* volatile保证变量在内存的可见性,但是不保证原子性
*
* **/
public class VolatileUnsafe {
public static void main(String[] args) {
//创建线程的一个实列
RunableThread runableThread = new RunableThread();
//下面四个线程共享nums
Thread A = new Thread(runableThread, "A:");
Thread B = new Thread(runableThread, "B:");
Thread C = new Thread(runableThread, "C:");
Thread D = new Thread(runableThread, "D:");
A.start();
B.start();
C.start();
D.start();
}
}
B:--nums=60
C:--nums=50
D:--nums=70
A:--nums=50
理论上输出的数据应该是20-40-60-80的,但是 结果很奇葩。这也就间接的说明。Volatile是线程不安全的。
ThreadLocal
可以理解成Map<Thread,Object>
作用: 主要作用是数据隔离,ThreadLocal修饰的变量,相当于一个副本每个线程都可以单独共享这个副本。也就是说被ThreaLocal修饰的变变量,只属于当前线程的。其他线程无法进行修改。每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。
举个例子。经ThreadLocal修饰的是变量nums=0。这时候与ABC三个线程去修改这个值)(nums+=1)。最终结果都是输出1,并不会出现1,2,3或者其他情况。
缺点:由于每个线程都共享一个副本,ThreadLocal修饰的变量不应该是占用内存大的变量比如对象、JSON等等。简称用空间换取线程的安全性。
代码实现
public class ThreadLocals {
//定义一个ThreadLocal修饰的变量。赋值默认值为1
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 1;
}
};
//创建三个线程
public void stardTreads(ThreadLocals threadLocals) {
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
threads[i] = creatRunable("线程["+i+"]",i);
}
//启动线程
for (Thread thread : threads) {
thread.start();
}
}
//线程
public Thread creatRunable(String threadName,Integer addNums) {
Thread thread = new Thread(() -> {
SleepTools.ms(100);
System.out.println(Thread.currentThread().getName() + "statr....");
threadLocal.set(threadLocal.get()+addNums);
//打印操作后的值
System.out.println(Thread.currentThread().getName() + "end...." + threadLocal.get());
SleepTools.ms(100);
},threadName);
return thread;
}
public static void main(String[] args) {
ThreadLocals threadLocals = new ThreadLocals();
threadLocals.stardTreads(threadLocals);
//等待所有线程结束后,打印下threadLocal里面的值
while (Thread.activeCount()>1){
System.out.println("threadLocal = "+threadLocals.threadLocal.get());
}
}
}
线程[1]statr....
线程[1]end....2
线程[0]statr....
线程[0]end....1
线程[2]statr....
线程[2]end....3
threadLocal = 1
我们发现输出的结果,线程0:0+1=1;线程1:1+1=2;线程2:2+1=3.。可以看见每个线程取ThreadLocal里面的值,都是相互独立的。而所有线程结束后,原来的默认值并没有改变。并且set后的值,并不影响一开始的Integer initialValue()方法设置的默认值,这说明数据已经被隔离了。
Wait和NotifyAll、Notify使用
如果对象调用了wait就会把当前锁释放,然后该线程进入wait等待队列中。
如果对象调用了notify方法就会唤醒wait等待队列中等待的线程。
如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。
场景:现在有ABC三个线程A负责输出A,B输出B,C输出C ,要求打印出五次ABC,ABC,ABC,ABC,ABC
/**
* 演示wait和notify\notifyAll
* 模糊场景:现在有ABC三个线程A负责输出A,B输出B,C输出C ,要求打印出五次ABC,ABC,ABC,ABC,ABC
**/
public class WnThread {
Integer flage = 1;
public synchronized void pritlnA() {
//如果flage不等于1,打印A的线程进入等待
while (flage != 1) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//输出A
System.out.print("A");
//接下来输出B
flage = 2;
//唤醒所有在等待的线程
notifyAll();
}
public synchronized void pritlnB() {
//如果flage不等于1,打印A的线程进入等待
while (flage != 2) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//输出A
System.out.print("B");
//接下来输出B
flage = 3;
notifyAll();
}
public synchronized void pritlnC() {
//如果flage不等于1,打印A的线程进入等待
while (flage != 3) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//输出A
System.out.print("C");
//接下来输出B
flage = 1;
notifyAll();
}
public static void main(String[] args) {
WnThread wnThread = new WnThread();
Thread threadA = new Thread(() -> {
for (int i = 0; i < 5; i++) {
wnThread.pritlnA();
}
});
Thread threadB = new Thread(() -> {
for (int i = 0; i < 5; i++) {
wnThread.pritlnB();
}
});
Thread threadC = new Thread(() -> {
for (int i = 0; i < 5; i++) {
wnThread.pritlnC();
}
});
//启动三个线程
threadA.start();
threadB.start();
threadC.start();
}
}
Join
作用: 获取执行权(插队,会让当前线程进入阻塞状态)。当线程A需要调用B线程的join方法时候,就必须等待着B的线程执行完成后,A线程才会继续执行。
举个例子:在食堂打饭的时候需要进行排队,这时候你看见有一个美女过来准备排队,你很高兴的把她叫到你前面进行排队了,这时候,你必须等待这个美女打完饭你才可以打饭。
public class JoinThread {
/**
* thread,线程A,线程B将插队,A线程直至B线程执行完,才会继续执行
* **/
public Thread createThreadA(Thread threadB){
return new Thread(()->{
try {
System.out.println("A线程开始执行。。。。。");
threadB.join();
System.out.println("A线程执行结束。。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A");
}
/**
* thread,线程B
* **/
public Thread createThreadB( ){
return new Thread(()->{
System.out.println(Thread.currentThread().getName()+"B线程在执行将休息三秒....");
SleepTools.sencod(3);
System.out.println("B线程休息结束.....");
});
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"--执行中--");
JoinThread joinThread = new JoinThread();
Thread threadB = joinThread.createThreadB();
Thread threadA = joinThread.createThreadA(threadB);
threadA.start();
threadB.start();
SleepTools.sencod(5);
System.out.println(Thread.currentThread().getName()+"---执行结束--");
}
}
看下面的输出结果,很明显看到A线程里面调用B的join方法时候,A必须等待B线程所有的代码执行完成后,才可以继续往下执行。即使B线程先执行,那A线程调用B的join,也必须等待B线程执行完
main--执行中--
A线程开始执行。。。。。
B线程在执行将休息三秒....
B线程休息结束.....
A线程执行结束。。。。。
main---执行结束--
yield、sleep、wait、notifyall
yield和sleep:都可以让线程进入睡眠状态的,但是他两个不会释放线程的持有锁。不同点在于,yield睡眠的线程时间到了之后,会进入就绪状态,线程可能再次继续执行。但是Sleep睡眠时间到了之后,会进入阻塞状态的,进行重新排队,不会再次执行。
wait:在使用wait进行线程等待之前,该线程一定要持有锁,调用wait方法,该锁会被释放掉,线程进入阻塞状态。
notify与notifyAll:作用都是唤醒被wait方法修饰的线程,但是不同的是,notify会随机唤醒一个等待的线程,但是notifyAll会唤醒所有等待的线程