1. 多线程编程基础
1.1 进程、线程
1.1.1 进程
狭义:进程是正在运行的程序的实例。
广义:进程是一个具有一定独立功能的程序,关于某个数据集合的一次运行活动。
进程是操作系统动态执行的基本单元,在传统的操作系统中, 进程既是基本的分配单元,也是基本的执行单元。
1.1.2 线程
线程是操作系统能够进行运算调试的最小单位。它被包含在进程中,是进程中的实际动作单位。一个线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每个线程执行不同的任务。
1.1.3 多线程的优点
- 可以把占据时间较长的任务放到后台去处理
- 程序的运行速度加快
1.3 线程常用API
1.3.1 currentThread方法
返回代码正在那个线程调用的详细信息
1.3.2 isAlive
是判断当前的线程是否处于活动状态。活动状态就是线程已经启动且运行没有结束。线程处于正在运行或准备开始运行的状态,就认为线程是『存活』的状态。
1.3.3 sleep方法
作用是在指定的毫秒数内让当前『正在执行的线程』暂停执行。
1.3.4 getId方法
作用获取当前线程的唯一标识
1.4 停止线程
停止一个线程意味着在线程处理完成任务之前结束正在执行的操作。
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。用一个boolean类型flag即可
- stop方法强制结束线程(stop方法已经被作废,如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个原因是对锁定的对象进行『解锁』,导致数据得不同同步的处理,出现数据不一致性的问题。)
- 使用interrupt方法中断线程 调用interrupt方法不会真正的结束线程,在当前线程中打上一个停止的标记。 Thread类提供了interrupted方法测试当前线程是否中断,isInterrupted方法测试线程是否已经中断。 复制代码
1.5 暂停线程
暂停线程使用suspend,重启暂停线程使用resume方法
public class Test {
public static void main(String[] args) throws InterruptedException {
Demo19Thread t = new Demo19Thread();
t.start();
Thread.sleep(1000);
t.suspend(); // 暂停线程
System.out.println("A=" + System.currentTimeMillis() + ", i=" + t.getI());
Thread.sleep(1000);
System.out.println("A=" + System.currentTimeMillis() + ", i=" + t.getI());
t.resume(); // 恢复暂停线程运行
Thread.sleep(1000);
t.suspend();
System.out.println("B=" + System.currentTimeMillis() + ", i=" + t.getI());
Thread.sleep(1000);
System.out.println("B=" + System.currentTimeMillis() + ", i=" + t.getI());
}
}
class Demo19Thread extends Thread{
private long i = 0;
public long getI(){
return i;
}
public void setI(long i){
this.i = i;
}
@Override
public void run() {
while (true){
i++;
}
}
}
suspend方法和resume方法都已经过时,它的缺点:
1.suspend方法暂停线程时,如果有锁的情况不会释放锁,容易造成其他线程无法持有锁,而无法进入run方法。
2.suspend方法和resume方法不是同步方法,可能造成脏读环境。
public class Test {
public static void main(String[] args) throws InterruptedException {
Demo22User user = new Demo22User();
Thread t1 = new Thread(){
@Override
public void run() {
user.updateUsernameAndPassword("b", "bb");
}
};
t1.setName("A");
t1.start();
Thread.sleep(10);
Thread t2 = new Thread(){
@Override
public void run() {
user.printUseruserAndPassword();
}
};
t2.start();
}
}
class Demo22User {
private String username = "a";
private String password = "aa";
public void updateUsernameAndPassword(String username, String password){
this.username = username;
if ("A".equals(Thread.currentThread().getName())){
System.out.println("停止A线程");
Thread.currentThread().suspend();
}
this.password = password;
}
public void printUseruserAndPassword(){
System.out.println("username=" + username + ", password=" +password);
}
}
1.6 yield方法
yield方法的作用是放弃当前的CPU资源,将资源让给其它的任务去占用CPU执行。但放弃时间不确定,有可能刚刚放弃,马上又获取CPU时间片。
1.7 线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到更多的CPU资源,也就CPU会优先执行优先级较高的线程对象中的任务。设置线程优先有助于帮助『线程调度器』确定在下一次选择哪个线程优先执行。
设置线程的优先级使用setPriority方法,优级分为1~10个级别,如果设置优先级小于1或大于10,JDK抛出IllegalArgumentException。JDK默认设置3个优先级常量,MIN_PRIORITY=1(最小值),NORM_PRIORITY=5(中间值,默认),MAX_PRIORITY=10(最大值)。
获取线程的优先级使用getPriority方法。
线程的优先级具有继承性,比如线程A启动线程B,线程B的优先级与线程A是一样的。 高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部执行完。当线程优先级的等级差距很大时,谁先执行完和代码的调用顺序无关。
线程的优先还有『随机性』,也就是说优先级高的线不一定每一次都先执行完成
1.8 守护线程
在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,特殊指的是当进程中不存在用户线程时,守护线程会自动销毁。典型的守护线程的例子就是垃圾回收线程,当进程中没有用户线程,垃圾回收线程就没有存在的必要了,会自动销毁。(设置守护线程必须要在调用start方法之前设置,否则JDK会产生
IllegalThreadStateException)
2. 线程的同步机制
2.1 如果多个线程同时读写共享变量,会出现数据不一致的问题。Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。synchronized取得的锁都是对象锁,而不是把一段代码或方法作为锁,所以哪个线程先执行带synchronized关键字修饰的方法,哪个方法就持有该方法所属对象的锁,其它线程只能呈等待状态,前提是多个线程访问的是同一个对象。如果多个线程访问多个对象,JVM会创建出多个对象锁。synchronized修饰普通方法获得的锁对象是this,synchronized修饰静态方法,获得的锁对象是当前类的字节码
2.2 锁重入
关键字synchronized拥有锁重入的功能,就是说在使用synchronized时,当一个线程得到一个对象锁后,再次请求些对象锁时是可以再次得到该对象锁。
同步synchronized(class)代码块的作用其实和synchronized static方法的作用是一样的
2.3 锁的自动释放
当一个线程执行的代码出现了异常,其所持有的锁会自动释放。
2.4 同步不具有继承性
2.5 使用任意对象作为对象锁 除了可以使用syncrhonized(this)来同步代码块,Java还支持『任意对象』作为『对象锁』来实现同步的功能。这个『任意对象』大数是成员变量或方法的参数,使用格式synchronized(非this对象)。
在多个线程持有『对象锁』为同一个对象的情况下,同一时间只有一个线程可以执行synchronized(非this对象)同步代码块中的代码。如果使用不是同一个对象锁,运行的结果就是异步调用,会交叉运行。
2.6 用字符串做对象,因为常量池,一样的字符串是同一个对象。用自定义类的实例做对象,类的成员变量变了,锁对象没变,因为该对象实例对应的内存地址没有变化
2.7 死锁
是指多个线程在运行过程中因争夺资源而造成的一种僵局,当线程处于这种僵持的状态时,如果没有外力作用,这些线程都无法再继续运行。
package chap2;
public class Demo23 {
public static void main(String[] args) throws InterruptedException {
Demo23Thread t = new Demo23Thread();
t.setFlag("a");
Thread t1 = new Thread(t);
t1.start();
Thread.sleep(10);
t.setFlag("b");
Thread t2 = new Thread(t);
t2.start();
}
}
class Demo23Thread extends Thread{
private String flag; // 标志,控制代码以什么样的方式运行
private Object lock1 = new Object();
private Object lock2 = new Object();
public void setFlag(String flag){
this.flag = flag;
}
@Override
public void run() {
try {
if ("a".equals(flag)) {
synchronized (lock1) {
System.out.println("flag=" + flag);
Thread.sleep(3000);
synchronized (lock2) {
System.out.println("按lock1=>lock2的顺序执行");
}
}
} else {
synchronized (lock2){
System.out.println("flag=" + flag);
Thread.sleep(3000);
synchronized (lock1){
System.out.println("按lock2->lock1的顺序执行");
}
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
2.8 synchronize和volatile区别:
- volatile是线程同步的轻易级实现,它的性能比synchronized要好,并且volatile只能修饰变量。而synchronized可以修饰方法及代码块。随着JDK的版本更新,synchronized在执行效率也得到很大的提升,在开发中synchronized的使用率还是较高
- 多线程访问volatile不会发生阻塞,而synhcronized会出现阻塞
- volatile能保证数据的可见性,不能保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
- volatile解决的是变量在多个线程之见的可见性,而synchronized是解决多个线程之间访问资源的同步性
总之,volatile解决的是变量在多个线程之见的可见性和有序性(防止指令重排),而synchronized是解决多个线程之间访问资源的同步性
3. 线程间的通信
线程是程序中独立的个体,但这些个体如果不经过处理就能成为一个整体。线程间的通信就是成为整体的必胜方案之一,可以说使线程间通讯后,线程之间的交互性会更强大,大大提高CPU复用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。
3.1 wait与notify
- wait/notifty机制的实现
wait方法的作用是使当前正在执行的线程进入等待状态,wait方法是Object类的方法,该方法用来将当前线程放入到『预执行队列』中,并且在wait所在的代码行进行停止执行,直到接到通知或被中断为止。在调用wait方法之前,线程必须获取得该对象的对象锁,也就是说只能在同步方法或同步代码块中调用wait方法。如果在执行wait方法后,当前线程锁会自动释放。当wait方法返回线程与其它线程重新竞争获得锁。
package chap3;
public class Demo03 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Demo03ThreadA(lock);
t1.start();
Thread.sleep(2000);
Thread t2 = new Demo03ThreadB(lock);
t2.start();
}
}
class Demo03ThreadA extends Thread{
private Object lock;
public Demo03ThreadA(Object lock){
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("线程A开始等待在" + System.currentTimeMillis());
lock.wait();
System.out.println("线程A结束等待在" + System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
class Demo03ThreadB extends Thread{
private Object lock;
public Demo03ThreadB(Object lock){
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("线程B准备发出通知在" + System.currentTimeMillis());
lock.notify();
System.out.println("线程B结束发出通知在" + System.currentTimeMillis());
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
- wait方法和notify方法都要在同步方法中调用,在调用前线程必须获得该对象的对象锁,如果没有获取重适当的锁也会抛出IllegalMonitorStateException。这个方法是用来通知那些可能等待锁对象的其它线程,如果有多个线程等待,由线程调试器随机挑选一个在wait状态的线程,向其发出通知,并使等待获取该对象的对象锁。
- 在执行notify方法后,当前线程不会马上释放该对象锁,wait状态的线程也不能马上获取取该对象锁,要等到执行notify方法的线程将任务执行完成后,也就是退出synchronize代码块后,当前线程才会释放锁,wait状态线程才可以获取到锁
- 当第一个获得该对象锁的wait线程运行完成后,它会释放掉该对象锁,如果该对象没有再次使用nofity语句,则对象处理空闲状态,其它 wait状态的线程由于没有得到通知,还会继续处理阻塞的wait状态,直到这个对象发现次发出通知
- wait方法自动释放锁与notify方法不会释放锁,notify方法必须执行会同步代码后才会释放锁
- 当线程呈wait状态时,调用线程对象的interrupt方法会产生InterruptedException异常
- 调用notify方法一次只随机通知一个线程进行唤醒。要唤醒所有的线程用notifyAll()
- 带一个long参数的方法的作用等待某一时间内是否有线程对象锁进行唤醒,如果超过这个等待时间线程会自动唤醒
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Demo04Thread(lock);
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
}
class Demo04Service{
public void foo(Object lock){
try{
synchronized (lock){
System.out.println("准备开始等待");
lock.wait();
System.out.println("结束等待");
}
}catch (InterruptedException e){
System.out.println("出现异常,因为wait状态的线程被interrupt了");
e.printStackTrace();
}
}
}
class Demo04Thread extends Thread{
private Object lock;
public Demo04Thread(Object lock){
this.lock = lock;
}
@Override
public void run() {
Demo04Service service = new Demo04Service();
service.foo(lock);
}
}
什么情况会释放锁?
- 执行完同步代码块就会释放对象锁;
- 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放;
- 在执行同步代码块的过程中,执行了锁所属对象的wait 方法,这个线程会释放对象锁,而此线程对象会进入线程等待池中等待被唤醒。
3.2 生产者/消费者模式实现(一个生产者与一个消费者)
public class Demo09 {
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Demo09Producer(lock);
t1.start();
Thread t2 = new Demo09Consumer(lock);
t2.start();
}
}
class Demo09VO{
public static String value = "";
}
// 生产者
class Demo09Producer extends Thread {
private Object lock;
public Demo09Producer(Object lock){
this.lock = lock;
}
@Override
public void run() {
try{
while(true) {
synchronized (lock) {
if (!"".equals(Demo09VO.value)){
lock.wait();
}
String value = System.currentTimeMillis() + "_" + System.nanoTime();
System.out.println("Set的值是:" + value);
Demo09VO.value = value;
lock.notify();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
class Demo09Consumer extends Thread{
private Object lock;
public Demo09Consumer(Object lock){
this.lock = lock;
}
@Override
public void run() {
try{
while(true){
synchronized (lock){
if ("".equals(Demo09VO.value)){
lock.wait();
}
System.out.println("Get的值是:" + Demo09VO.value);
Demo09VO.value = "";
lock.notify();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
如果在这个代码的基础上,设计出多生产者与多消费者,在运行过程很有可能会出现 『假死』的情况,也就是所有的线程都是WAIT状态
3.3 多生产与多消费者
public class Demo10 {
public static void main(String[] args) {
Object lock = new Object();
int size = 2;
Thread[] producers = new Thread[size];
Thread[] consumers = new Thread[size];
for (int i = 0; i < size; i++) {
char c = (char)('A' + i);
producers[i] = new Demo10Producer(lock);
producers[i].setName("生产者" + c);
consumers[i] = new Demo10Consumer(lock);
consumers[i].setName("消费者" + c);
producers[i].start();
consumers[i].start();
}
}
}
class Demo10VO{
public static String value = "";
}
// 生产者
class Demo10Producer extends Thread {
private Object lock;
public Demo10Producer(Object lock){
this.lock = lock;
}
@Override
public void run() {
try{
while(true) {
synchronized (lock) {
if (!"".equals(Demo10VO.value)){
System.out.println(Thread.currentThread().getName() + "等待中……");
lock.wait();
}
System.out.println(Thread.currentThread().getName() + "生产中……");
String value = System.currentTimeMillis() + "_" + System.nanoTime();
Demo10VO.value = value;
//lock.notify();
lock.notifyAll();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
class Demo10Consumer extends Thread{
private Object lock;
public Demo10Consumer(Object lock){
this.lock = lock;
}
@Override
public void run() {
try{
while(true){
synchronized (lock){
if ("".equals(Demo10VO.value)){
System.out.println(Thread.currentThread().getName() + "等待中……");
lock.wait();
}
System.out.println(Thread.currentThread().getName() + "消费中……");
Demo10VO.value = "";
// lock.notify();
lock.notifyAll();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
假设生产A它在生产数据,其它3个线程(生产者B、消费者A、消费者B)都是呈等待状态,当生产者A生产完成后,随机唤醒1个线程,刚好唤醒了生产B,生产者B发现ValueObject中有数据所以进行到等待状态(生产者A竞争锁,生产者B、消费者A、消费者B等待状态),A又重新获得锁但是它发现创建出来的值没还有被消费,所以它又进入等待状态,结果就是4个线程都是在待状态。怎样样解决这个问题,使用notifyAll方法把有的线程都唤醒,保证生产出来的值一定会被消费掉。