基础知识
多线程的实现方式
注意点
- Thread 也是实现了 Runnable 接口的类
- 启动一个线程中要执行的代码都Runnable接口的run()方法中实现
线程安全问题
线程安全问题是由于多个线程共同操作同一个变量和实例而产生的
- 在非原子操作中,一个线程多变量的操作还没有完成,另一个线程对这个变量进行操作,就导致错误的发生
- 在没有同步的方法中,线程是异步执行的
常用Thread类API
- currentThread() , 返回当前正在执行的线程
- isAlive() 用于测试这个线程是否处于活动状态
- sleep() ,让当前在指定的时间内处于阻塞状态,时间到了后,资源允许可以恢复为可执行状态。
sleep() 在阻塞时,不会释放锁,若方法不是同步方法,同样会造成不同步的问题
public class Test {
private String name = "sdf";
private String password = "zzp";
public void setValue(String u , String p) throws InterruptedException {
this.name = u;
if (Thread.currentThread().getName().equals("a")){
System.out.println("线程a休眠5秒");
Thread.currentThread().sleep(5000);
}
this.password = p;
}
public void printValue(){
System.out.println(name + " " + password);
}
public static void main(String[] args) {
Test test = new Test();
Thread thread1 = new Thread(()->{
try {
test.setValue("a" , "aa");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.setName("a");
thread1.start();
Thread thread2 = new Thread(()->{
test.printValue();
});
thread2.start();
}
}
- **geId()**用于获得线程ID
- yield() 让此线程放弃cpu资源,让给其他cpu,不是立即让出,让出的时间不一定
停止线程
- interrupt(),为当前线程打一个停止标记,不会立即停止标记
判断线程停止
- this.interrupted() : 判断此线程是否处于中断, 同时调用会会改变中断标志为false
- this.isInterrupted() : 测试线程是否处于中断,不会清楚状态标志
public boolean isInterrupted() {
return interrupted;
}
public static boolean interrupted() {
Thread t = currentThread();
boolean interrupted = t.interrupted;
if (interrupted) {
t.interrupted = false;
clearInterruptEvent();
}
return interrupted;
}
从jdk的源码也可以看出,判断中断的实现方式
停止线程的方法
- 异常法
class MyThread extends Thread {
private String name = "sdf";
private String password = "zzp";
@Override
public void run() {
try {
for (int i = 0; i < 50000; i++) {
if (this.interrupted()) {
System.out.println("停住了");
throw new InterruptedException();
}
System.out.println("i =" + (i + 1));
}
} catch (InterruptedException e) {
System.out.println("用catch停止了线程");
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
Thread.sleep(50);
myThread.interrupt();
}
}
- 使用return 停止线程
import com.sun.jdi.ClassType;
import java.io.*;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Random;
class MyThread extends Thread {
private String name = "sdf";
private String password = "zzp";
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
if (this.isInterrupted()) {
System.out.println("停住了");
return;
}
System.out.println("i =" + (i + 1));
}
for (int i = 0 ; i < 2 ; i ++) {
System.out.println("我又继续继续运行了 , 线程并没有停止");
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
Thread.sleep(10);
myThread.interrupt();
}
}
线程优先级
- 使用**setPriority()**可以设置优先级,优先级高的线程得到cpu资源的概率比较高
- A线程启动B线程,B线程与A线程的优先级是相同的
线程分类
- 用户线程
- 守护线程
- 守护线程的作用是为其他线程提供服务
synchronized 关键字的用法
- synchronized是对象锁
- synchronized,支持重入锁(一个线程获得对象锁后,再次请求该对象锁是可以再次得到的,称为可重入锁 , 一个synchronized 方法内部调用本类其他synchronized方法永远可以得到锁)
public class Server {
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized public void service2(){
System.out.println("service2");
service3();
}
synchronized public void service3(){
System.out.println("service3");
}
}
class Mythread extends Thread {
@Override
public void run() {
Server server = new Server();
server.service1();
}
}
public class Test {
public static void main(String[] args) {
Mythread t = new Mythread();
t.start();
}
}
可重入锁支持在子类调用父类的同步方法
public class Main {
public int i = 10;
synchronized public void operateIMainMethod (){
try {
i--;
System.out.println("main i =" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Sub extends Main{
synchronized public void operateISubMethod(){
try {
while (i > 0){
i --;
System.out.println("Sub i = " + i);
Thread.sleep(100);
this.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(()->{
Sub sub = new Sub();
sub.operateISubMethod();
}).start();
}
}
- 出现异常,锁自动释放
- 同步没有继承性 , 子类重写父类的方法,不会继承父类的同步
synchronized 同步块
- synchronized(this) , 锁定的是括号中的对象,可以使用任意对象作为对象监视器,只有括号中对象是相同的**synchronized(this)**才是同步,否则是异步的。
静态synchronized方法,synchronized(class)代码块
- 锁定的是,*.java文件对应的.class类
volatile 关键字
- 修饰的变量保证了可见性
- 修饰的变量不具有原子性
- synchronized代码块有volatile同步功能,将线程中内存与公共内存变量进行同步。
线程通信
通知等待机制
- wait():阻塞调用此方法的线程,在执行run方法中的代码后,此线程进入到此对象监视器的等待队列中。
- notify():随机唤醒一个在此对象监视器上的等待队列的线程
- notifyAll():唤醒所有在此对象监视器上的等待队列上的所有线程
等待队列
- 执行wait()方法后进入此对象监视器的等待队列,在等待状态必须有显示唤醒才能进入同步队列
阻塞装态与等待状态的区别:
- 阻塞状态是线程阻塞在获取锁的状态
- 等待状态是已经获取到了锁,执行了wait()方法,进入到等待队列
同步队列
- 对象监视器的锁别其它线程获取,所有想获得锁的线程都处在同步队列中等待获取锁。
利用通知等待机制实现生产者与消费者模式
- 单生产者和单消费者
import java.util.Random;
class P{
private String lock;
public P(){}
public P(String lock){
this.lock = lock;
}
public void setValue() throws InterruptedException {
synchronized (lock){
if (!ValueObject.value.equals("")){
lock.wait();
}
ValueObject.value = "" + (1 + Math.random()*10);
System.out.println("set=" + ValueObject.value);
lock.notify();
}
}
}
class C{
private String lock;
public C(){}
public C(String lock){
this.lock = lock;
}
public void getValue() throws InterruptedException {
synchronized (lock){
if (ValueObject.value.equals("")){
lock.wait();
}
System.out.println("get= " + ValueObject.value);
ValueObject.value = "";
lock.notify();
}
}
}
class ValueObject{
public static String value = "";
}
public class Test {
public static void main(String[] args) throws InterruptedException {
String lock = "";
P production = new P(lock);
C consumer = new C(lock);
Thread p = new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
production.setValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread c = new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
consumer.getValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
c.start();
p.start();
}
}
- 多生产者与多消费者
注意点
- 多生产者多多消费者中使用notify(),可能会造成死锁现象,因为nofify()是随机唤醒等待队列中的线程,如果唤醒的是同类的话,就会造成死锁,解决方法是使用notifyAll()
- 判断是否进入阻塞状态时,应该使用while循环判断消息队列中的消息
import java.util.Random;
class P{
private String lock;
public P(){}
public P(String lock){
this.lock = lock;
}
public void setValue() throws InterruptedException {
synchronized (lock){
while (!ValueObject.value.equals("")){
System.out.println(Thread.currentThread().getName() + "wait了");
lock.wait();
}
ValueObject.value = "" + (1 + Math.random()*10);
lock.notifyAll();
}
}
}
class C{
private String lock;
public C(){}
public C(String lock){
this.lock = lock;
}
public void getValue() throws InterruptedException {
synchronized (lock){
while (ValueObject.value.equals("")){
System.out.println(Thread.currentThread().getName() + "wait了");
lock.wait();
}
ValueObject.value = "";
lock.notifyAll();
}
}
}
class ValueObject{
public static String value = "";
}
public class Test {
public static void main(String[] args) throws InterruptedException {
String lock = "";
P production = new P(lock);
C consumer = new C(lock);
Runnable r1 = ()->{
for (int i = 0; i < 100; i++) {
try {
production.setValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable r2 = ()->{
for (int i = 0; i < 100; i++) {
try {
consumer.getValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 3; i++) {
new Thread(r1,"生产者"+i).start();
new Thread(r2,"消费者"+i).start();
}
}
}
通过管道进行线程间通信:字符流
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;
class WriteData{
public void write(PipedOutputStream out) throws IOException {
System.out.println("write:");
for (int i = 0; i < 10; i++) {
String outData = "" + (i+1);
out.write(outData.getBytes(StandardCharsets.UTF_8));
}
System.out.println();
out.close();
}
}
class ReadData{
public void readMethod(PipedInputStream input) throws IOException {
System.out.println("read :");
byte[] byteArray = new byte[20];
int readLength = input.read(byteArray);
while(readLength != -1){
String newData = new String(byteArray,0,readLength);
System.out.println(newData);
readLength = input.read(byteArray);
}
System.out.println();
input.close();
}
}
class ThreadWrite implements Runnable{
WriteData write;
private PipedOutputStream out;
public ThreadWrite(WriteData write , PipedOutputStream out){
this.write = write;
this.out = out;
}
@Override
public void run() {
try {
write.write(out);
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ThreadRead implements Runnable{
private ReadData read;
private PipedInputStream input;
public ThreadRead(ReadData read , PipedInputStream input){
this.input = input;
this.read = read;
}
@Override
public void run() {
try {
read.readMethod(input);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class TestPipe {
public static void main(String[] args) throws IOException, InterruptedException {
WriteData write = new WriteData();
ReadData read = new ReadData();
PipedInputStream input = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream();
out.connect(input);
Thread threadRead = new Thread(new ThreadRead(read , input));
Thread threadWrite =new Thread(new ThreadWrite(write ,out));
threadRead.start();
Thread.sleep(2000);
threadWrite.start();
}
}
重写回顾面向对象的思想
写这个程序的时候又重写思考了什么是面向对象的思想:
- 封装,什么是封装呢,学习了半年的面向对象,目前理解的面向对象就是,一个程序需要AB,C三个内容,把A,B,C封装为三个类,A,B,C只需要要知道其他类的对象有什么方法,不需要知道怎么实现的了。这也就形成了高内聚,低耦合,把一个模块集中在一块,只让其他模块知道自己有什么方法,不同模块之间没有联系,排查bug时可以精确定位到某一个模块。
- 继承,是为了提高代码的利用率,俩个模块相似度极高,A与B模块特别相似,B已经封装好了,此时如果再次重新封装A会有大量重复的代码,此时A继承B,改动A中部分代码就可以实现封装,减少了大量的重复代码
- 多态,多态是建立在继承的基础之上,用父类引用不同子类,调用不同的方法,会有不同的实现,这也避免了大量重复的代码。
关于join()
- 此方法的作用呢就是,将调用次此方法的线程进入等待状态,直到获得在此线程中调用的线程的锁
这个解释可能有点不清楚,先看一个简单的代码。
public class learnJoin {
public static void main(String[] args) throws InterruptedException {
Thread r = new Thread(()->{
System.out.println("准备join");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
r.start();
r.join();
System.out.println("main线程在r线程后执行");
}
}
//经过测试main线程总是在r线程之后执行
这是为什么呢,我们来看join的源代码
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) >0);
}
} else if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
- 首先join()是一个synchronized()的方法,说明在上面程序中main线程要执行join()方法线程得获得synchronized(this)锁,此处的this是什么呢应该是就在main线程中执行的线程了
- 也就是说,r.join()执行已经,main线程已经进入等待状态了,只有获得了线程r的锁,而且被唤醒后才能继续执行,我们从join()源码中看到了进入等待状态,但是唤醒在哪儿呢,这个经过查资料,JVM进行的唤醒。
ThreadLocal
此处先引用俩篇博客
- 关于ThreadLocal的讲解
- 关于Java引用
作用:
- 为不同的线程提供可以全局访问的
public class TestLocal {
public static ThreadLocal<String> t1 = new ThreadLocal<>();
public static void main(String[] args) {
Runnable r = ()->{
if (TestLocal.t1.get() == null){
System.out.println(Thread.currentThread().getName()+"ThreadLocal目前是Null");
}
TestLocal.t1.set(Thread.currentThread().getName() + "有值了");
System.out.println(t1.get());
};
for (int i = 0; i < 10; i++) {
new Thread(r,"" + i).start();
}
}
}
对于 Java Web 应用而言,Session 保存了很多信息。很多时候需要通过 Session 获取信息,有些时候又需要修改 Session 的信息。一方面,需要保证每个线程有自己单独的 Session 实例。另一方面,由于很多地方都需要操作 Session,存在多方法共享 Session 的需求。如果不使用 ThreadLocal,可以在每个线程内构建一个 Session实例,并将该实例在多个方法间传递,如下所示。
public class SessionHandler {
@Data
public static class Session {
private String id;
private String user;
private String status;
}
public Session createSession() {
return new Session();
}
public String getUser(Session session) {
return session.getUser();
}
public String getStatus(Session session) {
return session.getStatus();
}
public void setStatus(Session session, String status) {
session.setStatus(status);
}
public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
Session session = handler.createSession();
handler.getStatus(session);
handler.getUser(session);
handler.setStatus(session, "close");
handler.getStatus(session);
}).start();
}
}
该方法是可以实现需求的。但是每个需要使用 Session 的地方,都需要显式传递 Session 对象,方法间耦合度较高。
这里使用 ThreadLocal 重新实现该功能如下所示。
public class SessionHandler {
public static ThreadLocal<Session> session = ThreadLocal.<Session>withInitial(() -> new Session());
@Data
public static class Session {
private String id;
private String user;
private String status;
}
public String getUser() {
return session.get().getUser();
}
public String getStatus() {
return session.get().getStatus();
}
public void setStatus(String status) {
session.get().setStatus(status);
}
public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
handler.getStatus();
handler.getUser();
handler.setStatus("close");
handler.getStatus();
}).start();
}
}
使用 ThreadLocal 改造后的代码,不再需要在各个方法间传递 Session 对象,并且也非常轻松的保证了每个线程拥有自己独立的实例。
如果单看其中某一点,替代方法很多。比如可通过在线程内创建局部变量可实现每个线程有自己的实例,使用静态变量可实现变量在方法间的共享。但如果要同时满足变量在线程间的隔离与方法间的共享,ThreadLocal再合适不过。
InheritableThreadLocal的区别
- 区别在与InheritableThreadLocal类可以在子线程从父线程中取得继承下来的值
ReentranLock类
- 是一个互斥锁,lock也是对象锁
Condition类
- **await()**与wait()方法类似
- **signal(),signalAll()**与notify(),notifyAll()方法类似
实现的单消费者与单生产者,多消费者和生产者与上面的用wait的类似
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Son {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
boolean flag = true;
public void son() throws InterruptedException {
lock.lock();
System.out.println("ljh you are my son , 请说父亲好");
while(flag){
condition.await();
}
flag = true;
condition.signal();
lock.unlock();
}
public void father() throws InterruptedException {
lock.lock();
while(!flag){
condition.await();
}
System.out.println("我是你father , 乖儿子");
flag = false;
condition.signal();
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
Son ljh = new Son();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
ljh.son();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// Thread.sleep(1000);
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
ljh.father();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
公平锁与非公平锁
- 所谓公平与非公平,就是进入等待队列后,唤醒的顺序,如果按照进入等待队列是时间唤醒,那么就是公平的,随机唤醒就不公平的。
- **ReentranLock()**支持公平锁与非公平锁
ReentranReadWriteLock类
- 只要和写有关线程都是互斥锁
- 只有读的情况是共享锁
乐观锁与悲观锁
悲观锁认为线程拿到锁,一定会对变量进行脏读,拿到锁后,其他线程就进入阻塞状态
- synchronized , Lock锁都是悲观锁
乐观锁,总是乐观地假设最好的情况,每次去拿数据的时候都认为别人不会修改这个数据,所以不会上锁,只会要对数据进行更新时判断一下在此期间(拿到数据到更新的期间)别人有没有去更改这个数据,可以使用版本号机制和CAS算法实现。
CAS 机制
AS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
举个例子
- 线程1拿到了变量a = 10,
- 此时发生了线程切换,运行在线程2上
- 线程2将a 更新为11
- 切换为线程1 , 内存地址与旧值A对比,发现不同,修改失败,然后重新获取(此过程称为自旋)
- 线程再次进行比较
缺点
CAS的缺点:
1.CPU开销较大 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性 CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
偏向锁,轻量级锁,重量级锁
要是分清这几个概念首先必须知道对象头这个东西
对象头
对象头是java中对象都具有的属性,是jvm在编译和运行阶段读取的信息。对象头包含三个部分:
- mark word
- 指针向类的指针
- 数组的长度(只有数组的对象用到)
这3个中最复杂的是MarkWord,MarkWord用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
偏向锁
当只有一个线程重复同一个锁时,使用阻塞机制,消耗太大。便有了偏向锁的概念
什么是偏向锁,顾名思义就偏向一个线程的锁,
偏向锁的过程
- 一个线程访问同步块时,在锁的对象监视器的对象头中,在栈帧中获取锁偏向的线程id,
- 以后该线程获取锁时,只需判断对象监视器的对象头的markWork是否存在该线程的偏向锁,
- 成功,则获取锁,
- 失败,查看此对象头偏向锁标识是否设置为1 ,如果设置为1 (说明是偏向锁但是还没有任何线程获取),所以将使用CAS机制将对象头的偏向锁指向当前线程的线程id
- 查看此对象头偏向锁标识是否设置为1 ,如果没有设置为1,则说明已经不是偏向锁了,需要用CAS机制获取锁
偏向锁的撤销
偏向锁在出现竞争时,释放锁 ,需要run内代码执行完
- 出现竞争时
- 把当前的在执行的线程1的run方法执行完,暂停该线程
- 检查线程是否存活
- 不存活,偏向锁标志设置为0
- 线程2使用CAS机制,将偏向锁指向线程2
- 线程1仍然存在,暂停线程1;
- 设置锁标志位为00(变为轻量级锁),偏向锁为0;
轻量级锁
加锁
- 将对象的mark word 复制到自己空间中
- 使用CAS机制将Mark word 替换为指向自己锁记录的指针
- 成功,则获取锁,失败,通过自旋获取
解锁
- 解锁时,使用CAS操作将对象头中的Mark word换为原来的内容,就是线程中存储的次对象头的Mark word换回去
- 成功,释放锁
- 失败,说明有竞争的线程,此时膨胀为重量级锁
轻量级锁,竞争锁是不断通过自选获取锁,会占用资源
重量级锁,直接将竞争的线程进行阻塞,不会占用CPU资源
|