Java多线程1
创始人
2024-02-07 06:58:35
0

文章目录

  • 一、对象及变量的并发访问(理论)
  • 二、Java多线程通信(实践)
    • 1.多生产与多消费(操作栈)
    • 2.在管道中传递字节流
    • 3.在管道中传递字符流
    • 4.ThreadLocal
    • 5.InheritableThreadLocal
  • 总结

一、对象及变量的并发访问(理论)

  1. 脏读(dirty read),发生脏读的原因是在读取实例变量时,此值已经被其他线程改过了。
    即不同线程在读取和写同时操作,“争抢”实例变量导致的结果。此时变量至少为2个。
    可以使用synchronized关键字实现同步来解决,即获得锁的线程执行完,释放锁后,其他线程才能执行。也可使用读写锁。
  2. synchronized拥有重入锁,指自己可以再次获取自己的内部锁。
    即拿到一个对象锁之后,在未释放之前可以继续获取这个对象锁。
    也就是说访问一个synchronized修饰的方法后,可以访问该对象内的全部synchronized修饰的方法。
    this关键字指当前对象,将this作为锁时,当前对象的所有synchronized修饰的方法或this作为锁的代码块同一时间都由一个线程同步访问。
  3. holdsLock()是当currentThread在指定的对象上保持锁定时,才返回true。
  4. synchronized方法将当前对象作为锁,synchronized代码块将任意对象作为锁。
    即前者对象中被修饰的全部方法都是同步的,而后面可灵活使用同步,可以选择单个方法或多个方法,并且可以选择方法中的部分或者整体。
  5. synchronize(string)同步块与string联合使用,注意jvm的常量池带来的意外。即用string作为锁若在常量池中存在,2个线程的string相同,则会导致原本是异步执行的两个线程变成同步执行。
    synchronized代码块不使用string作为锁对象。
  6. java线程死锁,因为不同的线程都在等待根本不可能释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程“假死”。
    即线程之间相互等待对方的锁。
  7. 内置类和静态内置类,内置类是在类实例化之后,调用并new一个实例中的内置类。
    而静态修饰的内置类,则可以直接new一个静态内置类。
    使用static修饰内置类,可以将new出来的内置类实例作为唯一锁,从而使代码块和内置类中用synchronized关键字修饰的方法同步。
  8. 锁对象改变,会使同步执行变为异步执行。
    锁对象不变,而锁对象属性的改变,也可以同步执行。
  9. volatile关键字
    可见性,b线程可以马上看到a线程更改的数据。
    原子性,64位系统,原子性取决于具体的实现。
    禁止代码重排序。
    可见性
    解决死循环,通过另一个线程来改变循环的条件,使其结束循环。
    变量存在于公共堆栈及线程的私有堆栈中,线程运行后,一直在线程的私有堆栈中取变量的值。此时更改变量的值,是更改在公共堆栈的变量的值。
    此时使用volatile关键字修饰锁变量,强制让当前线程访问公共堆栈中的变量,而不在线程的私有堆栈中取值。
    synchronized关键字,可以使多个线程访问同一个资源具有同步性,而且具有使线程工作内存中的私有变量与公共内存中的变量同步的特性,即可见性。

原子性
即volatile和synchronized关键字都可以实现可见性和原子性。
volatile使用的主要场合是在多个线程中可以感知实例变量被更改了。从而实现了增加可见性和原子性。
atomic原子类实现原子性,可以在没有锁的情况下实现线程安全。
atomic原子类的方法是原子的,但是方法中atomic原子类多次调用方法,此时方法与方法之间是非原子的。只能用synchronized关键字修饰来实现方法与方法之间的原子性。

禁止代码重排序
java程序运行时,JIT(Just-In-Time Compiler)即时编译器可以动态地改变程序代码运行的顺序。
重排序是将轻耗时的代码执行完后,释放cpu资源给其他重耗时的代码,从而实现更高的程序运行效率。
重排序发生在没有依赖关系时,代码之间有关系则代码不会重排序。

使用volatile关键字修饰,可以禁止代码重排序。
但是仅volatile关键字修饰的变量禁止重排序,且以该变量为中心将代码分割成2部分,这两部分可以分别独立地进行重排序。与synchronized相同。
总结
volatile关键字和synchronized关键字都具有增加可见性、原子性和禁止重排序。
使用场景
volatile,当想实现一个变量的值被更改时,让其他线程能取到最新的值时,就要对变量使用volatile。
synchronized,当多个线程对同一个对象中的同一个实例变量进行操作时,为了避免出现非线程安全问题,就要使用synchronized。

二、Java多线程通信(实践)

1.多生产与多消费(操作栈)

  1. 创建一个signal.entity包,创建MyStack类

    package signal.entity;import java.util.ArrayList;
    import java.util.List;public class MyStack
    {//生成list对象private List list = new ArrayList<>();synchronized public void push(){try{while(list.size()==1){this.wait();}list.add("anyString="+Math.random());this.notifyAll();System.out.println("push="+list.size());} catch (InterruptedException e) {e.printStackTrace();}}//弹出synchronized public String pop(){String returnValue = "";try{while(list.size()==0){System.out.println("pop操作中的:"+Thread.currentThread().getName()+"线程wait状态");this.wait();}returnValue =""+list.get(0);list.remove(0);this.notifyAll();System.out.println("pop="+list.size());} catch (InterruptedException e) {e.printStackTrace();}return returnValue;}
    }

    MyStack类,主要作用是生成控制这个操作栈的推入和弹出方法。
    在方法内部使用While进行判断,因为存在多个消费者,如果使用if,则判断过后多个消费者就会执行remove,此时会报错,而使用While可以使没有拿到锁的对象继续wait。
    以及使用notifyAll(),使用多消费者或多生产者时,防止notify()唤醒同类型线程,导致线程假死。

  2. 创建一个signal.service包,创建P类

    package signal.service;import signal.entity.MyStack;public class P {private MyStack myStack;public P(MyStack myStack){super();this.myStack = myStack;}public void pushService(){myStack.push();}
    }

    即在生产者类P中内聚一个MyStack对象,由构造方法给该MyStack对象赋值,调用MyStack的push方法。

  3. 创建一个signal.service包,创建C类

    package signal.service;import signal.entity.MyStack;public class C {private MyStack myStack;public C(MyStack myStack){super();this.myStack = myStack;}public void popService(){System.out.println("pop="+myStack.pop());}
    }

    即在消费者类C中内聚一个MyStack对象,由构造方法给该MyStack对象赋值,调用MyStack的pop方法。

  4. 创建一个signal.thread包,创建P_Thread类

    package signal.thread;import signal.service.P;public class P_Thread extends Thread{private P p;public P_Thread(P p){super();this.p=p;}@Overridepublic void run() {while(true){p.pushService();}}
    }

    即通过构造方法将消费者P赋值给内聚对象P,再使用While调用P类的pushService()方法。

  5. 创建一个signal.thread包,创建C_Thread类

    package signal.thread;import signal.service.C;
    import signal.service.P;public class C_Thread extends Thread{private C c;public C_Thread(C c){super();this.c=c;}@Overridepublic void run() {while(true){c.popService();}}
    }

    即通过构造方法将消费者C赋值给内聚对象C,再使用While调用C类的popService()方法。

  6. 在signal包中创建Test测试类

    package signal;import signal.entity.MyStack;
    import signal.service.C;
    import signal.service.P;
    import signal.thread.C_Thread;
    import signal.thread.P_Thread;public class Test {public static void main(String[] args) {//操作栈对象MyStack myStack = new MyStack();//2个生产者P p1 = new P(myStack);P_Thread pThread1 = new P_Thread(p1);P p2 = new P(myStack);P_Thread pThread2 = new P_Thread(p2);//3个消费者C c1 = new C(myStack);C_Thread cThread1 = new C_Thread(c1);C c2 = new C(myStack);C_Thread cThread2 = new C_Thread(c2);C c3 = new C(myStack);C_Thread cThread3 = new C_Thread(c3);//生产者线程启动pThread1.start();pThread2.start();//消费者线程启动cThread1.start();cThread2.start();cThread3.start();}
    }

    最后进行多对多操作栈的测试,实例化一个操作栈对象,给全部线程进行构造方法赋值。
    使用2个生产者线程以及3个消费者线程并启动。
    即多个线程抢一个锁,并且一次只能操作一个。

2.在管道中传递字节流

  1. 创建一个signal.service包,在包下创建一个WriteData类

    package signal.service;import java.io.IOException;
    import java.io.PipedOutputStream;public class WriteData {public void writeMethod(PipedOutputStream out)  {try {System.out.println("write:");for (int i = 0; i < 300; i++) {String outData = ""+(i+1);out.write(outData.getBytes());System.out.print(outData);}System.out.println();out.close();} catch (IOException e) {throw new RuntimeException(e);}}
    }

    即在WriteData类的writeMethod方法中实现操作字节输出流PipedOutputStream

  2. 创建一个signal.service包,在包下创建一个ReadData类

    package signal.service;     
    import java.io.IOException;
    import java.io.PipedInputStream;public class ReadData {public void readMethod(PipedInputStream input) {try{System.out.println("read:");byte[] bytes = new byte[20];//将input中的字节流读取到字节数组byte中,一次读取20个。int readLength = input.read(bytes);while(readLength != -1){//从字节数组byte中,读取从input字节流中读取到的全部字节并存储到newData中。String newData = new String(bytes,0,readLength);System.out.print(newData);//将input中的字节流读取到字节数组byte中,一次读取20个。readLength = input.read(bytes);}System.out.println();input.close();} catch (IOException e) {throw new RuntimeException(e);}}
    }
    

    即在ReadData类的readMethod方法中实现操作字节输入流PipedInputStream

  3. 创建一个signal.thread包,在包下创建一个WriteThread类

    package signal.thread;import signal.service.WriteData;import java.io.PipedOutputStream;public class WriteThread extends Thread{private WriteData write;private PipedOutputStream out;public WriteThread(WriteData write,PipedOutputStream out){this.write = write;this.out = out;}@Overridepublic void run() {write.writeMethod(out);}
    }

    即用构造方法给WriteData和PipedOutputStream对象赋值。在输入流线程的run方法中调用WriteData对象的writeMethod,并将PipedOutputStream 对象作为参数传入到writeMethod方法中。
    PipedOutputStream对象是输出字节流对象。

  4. 创建一个signal.thread包,在包下创建一个ReadThread类

    package signal.thread;import signal.service.ReadData;import java.io.PipedInputStream;public class ReadThread extends Thread{private ReadData read;private PipedInputStream input;public ReadThread(ReadData read, PipedInputStream input){this.read = read;this.input = input;}@Overridepublic void run() {read.readMethod(input);}
    }

    即用构造方法给ReadData 和PipedInputStream对象赋值。在输入流线程的run方法中调用ReadData 对象的readMethod,并将PipedInputStream 对象作为参数传入到readMethod方法中。
    PipedInputStream对象是输入字节流对象。

  5. 在signal包下创建Run类

    package signal;import signal.service.ReadData;
    import signal.service.WriteData;
    import signal.thread.ReadThread;
    import signal.thread.WriteThread;import java.io.IOException;
    import java.io.PipedInputStream;
    import java.io.PipedOutputStream;public class Run {public static void main(String[] args) {try {WriteData writeData = new WriteData();ReadData readData = new ReadData();//实例化字节输入输出流对象PipedOutputStream pipedOutputStream = new PipedOutputStream();PipedInputStream pipedInputStream = new PipedInputStream();//将输入输出流对象进行connect关联pipedInputStream.connect(pipedOutputStream);//实例化读写线程ReadThread readThread = new ReadThread(readData, pipedInputStream);readThread.start();Thread.sleep(2000);WriteThread writeThread = new WriteThread(writeData, pipedOutputStream);writeThread.start();} catch (IOException e) {throw new RuntimeException(e);} catch (InterruptedException e) {throw new RuntimeException(e);}}
    }

    即该测试类实例化读写线程时,通过构造方法将WriteData(ReadData)对象和字节流对象传入并给内聚对象赋值。
    在run方法中调用WriteData(ReadData)的wirteMethod(readMethod)方法,wirteMethod(readMethod)方法则去实现将字节流转换成字符串(字符串转换成字节流)的形式输出(输入)。
    即Thread对象->Data.Method->数据操作

3.在管道中传递字符流

字符流跟字节流进行线程通信的实现方式相同,只是流对象不同,字符流是PipedReader和PipedWriter。

4.ThreadLocal

ThreadLocal主要作用是将数据放入当前线程对象中的Map里。这个Map是Thread类的实例变量。
数据->ThreadLocal->currentThread()->Map
Map的key是ThreadLocal对象,value是存储的值,每个线程中Map的值只对当前线程可见,其他线程不可以访问当前对象中的Map的值。
ThreadLocal对象由JVM进行实例化。
threadLocals变量是ThreadLocalMap实例,ThreadLocalMap又是属于Thread对象,默认是包级访问,不能从外部直接访问该变量。ThreadLocal和Thread在同一个包中,所以可以使用ThreadLocal对象访问Thread中的ThreadLocalMap这个变量。
ThreadLocalMap使用Entry对象来存储key也就是ThreadLocal对象及value也就是传入的值。然后再将Entry对象存入数组table中。
实现步骤,即创建一个类,用来实例化并存放ThreadLocal对象。这个类继承ThreadLocal类,然后重写initialValue方法以实现第一次get的值不为null。
线程在使用时,引用该实例,并调用set方法进行存储数据,get取值即可。
ThreadLocalMap中的静态内置类Entry是弱引用类型。
当垃圾回收器(gc)扫描时发现弱引用的对象,则不管内存是否足够,都会回收弱引用对象。
即执行gc操作,ThreadLocal对象就立即销毁。代表key的值ThreadLocal对象会随着gc操作而销毁,释放内存空间,但value值却不会随着gc操作而销毁,导致内存溢出。可以通过调用ThreadLocal实例的remove方法进行清除。
即在ThreadLocalMap中的数据不再使用时,要手动执行ThreadLocal的remove方法。

5.InheritableThreadLocal

使用InheritableThreadLocal可以在子线程中取得父线程继承下来的值。
main线程即父线程,在main线程中进行调用InheritableThreadLocal实例的set方法进行赋值。
线程类继承Thread,再调用InheritableThreadLocal实例的get方法取值,此时能取到main线程存入的值。
即子线程获取的值是从父线程main继承的。
此时set方法还是ThreadLocal的set方法,因为
InheritableThreadLocal没有重写set方法。
通过在创建子线程时,子线程主动引用父线程里面的InheritableThreadLocal对象值。从而实现子线程继承父线程中InheritableThreadLocal对象的值。
init自动被Thread的构造方法调用,最后一个参数为boolean inheritThreadLocals代表当前线程是否会从父线程中继承值,这个值被永远传入true。即每一次都会继承父线程的值。
并且当父线程中的InheritableThreadLocal对象值不为空时,将父线程的InheritableThreadLocal对象值赋值给子线程的InheritableThreadLocal对象值。
string数据类型是不可变的,对string赋新的常量值会开辟新的内存空间。
反言之,可变的就是2个对象引用同一个地址,一个对象的属性改变,另一个也能感应到。即赋值时非开辟新的内存空间,而是从常量值中去找值,没有才开辟新的内存空间。
不可变类型,当子线程有最新值,父线程还是旧值;当父线程有最新值,子线程还是旧值。
可变类型,当父线程有最新值,子线程也是最新值;子线程有最新值,父线程也是最新值。
childValue方法,将传入的参数(父线程中的值)进行返回,重写时可以添加其他的数据。

总结

  1. 变量的并发访问
    (1)执行完notify后,按照执行wait的顺序唤醒其他线程,notify所在的同步代码块执行完才会释放对象锁,最后其他线程继续执行wait后的代码。
    (2)在同步代码块中,遇到异常导致线程终止,锁也会被释放。
    (3)在同步代码块中,执行了锁对象的wait方法,这个线程会立即释放对象锁,等待被唤醒。
    (4)一生产一消费可以使用wait/notify机制,条件判断可以用if。
    其中只要有一个属于多时,就要使用wait/notifyAll,并且条件判断要用while。

  2. wait,线程就进入等待状态,即暂停执行,并释放锁,直到调用notify()。即wait立即释放锁。wait是object类的方法。
    sleep不释放锁,从而能实现同步效果。
    在同步方法或同步块中调用wait()和notify(),即必须拿到锁对象,才能调用wait()和notify()。
    wait(long),没有线程对锁进行notify,则超过这个时间后线程自动唤醒,此时等待重新持有锁。

  3. notify,顺序唤醒一个处于wait状态的线程,使其进入就绪状态。并且当前线程调用notify()后,需要当前线程执行完程序后,也就是退出synchronized代码块后,才会释放对象锁。即不立即释放锁。
    如果不存在wait状态的线程,则notify会被忽略。
    notify按照调用wait方法的线程顺序进行唤醒。
    notifyAll,逆序依次唤醒全部处于wait状态的线程,使其进入就绪状态。
    notify及notifyAll唤醒wait线程的顺序不是固定的,唤醒的顺序有正序、倒序、随机,取决于具体的JVM实现。

  4. 多生产与多消费,进入假死状态,就是线程进入waiting状态,如果全都线程都进入waiting状态,则程序就不再执行任何业务功能了,整个项目呈停止状态。
    使用wait/notify机制,但是notify唤醒的可能是同类,即生产者唤醒生产者,消费者唤醒消费者,从而导致所有线程进入waiting状态,即假死。可以使用wait/notifyAll机制来解决“假死”问题。

  5. 一生产多消费时,需要使用while做条件判断,防止条件发生改变时没有得到及时响应,以至于多个呈wait状态的线程被唤醒。
    就是说如果使用if进行条件判断,则它只判断一次条件,后面执行消费操作。因为生产者线程刚开始还未生产,多个消费线程都执行了if,都处于后面的消费操作;生产者生产后,消费者进行消费,后面notify或notifyAll又唤醒消费者,此时已经没有商品进行消费了,此时就会报错。
    与多生产多消费相同,需要使用wait/notifyAll机制防止假死。
    多生产一消费、多生产多消费与一生产多消费基本相同。即创建的线程实例数量不相同。

  6. 连续生产多个商品与连续消费多个商品。
    就是生产者线程商品后存储商品的容量有一个最大值;消费者线程从这个容量里取,然后进行消费。
    此时生产者和消费者还是每次能操作一个商品,但是能使用while连续多次操作,直到容量满/空。
    就是说生产者的生产线程多个,并且可以连续消费,还有一个生产检查和一个消费检查容量数量的线程。
    生产检查线程,当数据没有达到最大值,则notifyAll,唤醒所有线程进行生产及消费,并且该生产检查线程进入休眠状态。
    生产线程使用while检查容量是否达到最大值,达到最大值则进入wait状态,等待商品被消费线程消费。否则就生产一个商品,以此循环。
    消费检查线程,检查容量是否不为空,则notifyAll,唤醒所有线程进行生产及消费,并且该消费检查线程进入休眠状态。
    消费线程,使用while检查容量是否为空,为空则进入wait状态,等待商品被生产线程生产。否则就消费一个商品,以此循环。

  7. 通过管道进行线程间通信。
    Java JDK提供了4个类来使线程间可以进行通信,即PipedInputStream和PipedOutputStream、PipedReader和PipedWriter。
    字节流通信PipedInputStream和PipedOutputStream与字符流通信PipedReader和PipedWriter一样,创建实例后需要使用connect方法进行通信连接。
    即input连接output或者反过来都可以。
    字节流用来处理图片、图像、ppt和word文件等,也可以处理纯文本文件。
    字符流用来处理纯文本文件,但是不能处理图片等非纯文本文件。

  8. join方法,作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期阻塞,等待线程x销毁后再继续执行线程z后面的代码,具有串联的效果。
    类似于同步的运行效果,与synchronized不同的是,join使用wait(),而synchronized使用锁作为同步。
    join和interrupt方法共同使用且相遇时,会出现异常。
    join(long),用于指定等待的时间,不管x线程是否执行完毕,时间到了并且重新获得锁,则当前线程会继续向后执行。如果没有重新获得锁,则一直尝试,直到获得锁为止。
    因为join(long),内部使用wait(long)来实现,所以join(long)具有释放锁的特点。
    sleep(long)不释放锁。
    join(long millis,int nanos),即millis毫秒+nanos纳秒为该线程终止的最长时间,nanos取值范围是0~999999。
    使用join,即相当于让该线程调用wait方法。

相关内容

热门资讯

怎样选到靠谱刑事律师?赵可律师... 靠谱刑事律师的衡量标准在寻找靠谱的刑事律师时,有多个衡量标准。 专业能力是关键,律师需具备扎实的法学...
吉林省刑辩律师哪家强?辛明律师... 吉林省刑辩律师的重要性在吉林省,刑事案件的复杂性和多样性使得刑辩律师的作用愈发凸显。 他们不仅要熟悉...
江苏多地推出公租房调换政策 就... 原题:就医养老更方便 按需调换更贴心 公租房也能“换着住” 公共租赁房是由政府提供支持,为中低收入困...
法治日报:跨境犯罪治理需要更完... 跨境犯罪呈现多重犯罪形态交织特征 各国代表建言 跨境犯罪治理需要更完善的司法保障 编者按 携手30年...
原创 刘... 2025年12月18日,海南自由贸易港全岛封关运作正式启动,标志着我国高水平对外开放进入新阶段。全球...
美联储内部分歧加剧:哈马克称政... 智通财经APP获悉,克利夫兰联邦储备银行行长贝丝·哈马克表示,在评估第一季度累计75个基点的降息对经...
【深圳特区报】深港融通新格局 ... 前海港资企业突破万家、累计105项制度创新成果在全国复制推广、现代服务业增加值达1460亿元……12...
犯罪对象和受贿数额认定问题分析 实践中,有的行贿人为了送给国家工作人员好处,不直接送给国家工作人员财物,而是先委托国家工作人员代为出...
用好制度创新“加速器” 制度创新是破解发展难题、激发区域活力的核心密钥。上海浦东开发开放30余载的实践证明,唯有以制度创新破...