Java多线程

进程:正在进行中的程序。其实进程就是一个应用程序运行时的内存分配空间。进程负责的是应用程序的空间的标示。

线程:其实就是进程中一个程序执行控制单元,一条执行路径。线程负责的是应用程序的执行顺序。

  • 一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序。
  • 每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。

jvm在启动的时,首先有一个主线程,负责程序的执行,调用的是main函数,主线程执行的代码都在main方法中。当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样主线程中的代码执行会停止,而去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。 

随机性的原理:哪个线程获取到了cpu的执行权,哪个线程就执行,实质是cpu的快速切换造成。

返回当前线程的名称:Thread.currentThread().getName();线程的名称是由:Thread-编号定义的。编号从0开始。线程要运行的代码都统一存放在了run方法中。

线程要运行必须要通过类中指定的方法【start方法】开启。(启动后,就多了一条执行路径)

start方法

  1. 启动了线程
  2. 让jvm调用了run方法。

创建线程的第一种方式:继承Thread ,由子类复写run方法:

步骤:

  1. 定义类继承Thread类;
  2. 目的是复写run方法,将要让线程运行的代码都存储到run方法中;
  3. 通过创建Thread类的子类对象,创建线程对象;
  4. 调用线程的start方法,开启线程,并执行run方法。

【可能的运行结果】:

为什么我们不能直接调用run()方法呢?原因是线程的运行需要本地操作系统的支持。

查看start的源代码发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public synchronized void start() { 
      
       if (threadStatus != 0
           throw new IllegalThreadStateException(); 
   
       group.add(this); 
   
       boolean started = false
       try
           start0(); 
           started = true
       } finally
           try
               if (!started) { 
                   group.threadStartFailed(this); 
               
           } catch (Throwable ignore) { 
               /* do nothing. If start0 threw a Throwable then
                 it will be passed up the call stack */ 
           
       
   
   
private native void start0(); 

这个方法用了native关键字,native表示调用本地操作系统的函数,多线程的实现需要本地操作系统的支持。

线程状态

  • 被创建:start()
  • 运行:具备执行资格,同时具备执行权;
  • 冻结:sleep(time),wait()—notify()唤醒;线程释放了执行权,同时释放执行资格;
  • 临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
  • 消亡:stop()

创建线程的第二种方式:实现一个接口Runnable:

步骤:

  1. 定义类实现Runnable接口。
  2. 覆盖接口中的run方法(用于封装线程要运行的代码)。
  3. 通过Thread类创建线程对象;
  4. 将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。【为什么要传递呢?因为要让线程对象明确要运行的run方法所属的对象】
  5. 调用Thread对象的start方法,开启线程,并运行Runnable接口子类中的run方法。

Ticket t = new Ticket();

直接创建Ticket对象,并不是创建线程对象。【因为创建线程对象只能通过new Thread类,或者new Thread类的子类才可以

Thread t1 = new Thread(t); //创建线程。

只要将t作为Thread类的构造函数的实际参数传入即可完成线程对象和t之间的关联。【为什么要将t传给Thread类的构造函数呢?其实就是为了明确线程要运行的代码run方法】

t1.start();//开启线程

为什么要有Runnable接口的出现?

1:通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类,因为java单继承的局限性。

可是该类中的还有部分代码需要被多个线程同时执行,这时怎么办呢?

只有对该类进行额外的功能扩展,java就提供了一个接口Runnable。这个接口中定义了run方法,其实run方法的定义就是为了存储多线程要运行的代码。

所以,通常创建线程都用第二种方式。【因为实现Runnable接口可以避免单继承的局限性】

2:其实是将不同类中需要被多线程执行的代码进行抽取。将多线程要运行的代码的位置单独定义到接口中。为其他类进行功能扩展提供了前提。

所以Thread类在描述线程时,内部定义的run方法,也来自于Runnable接口。

实现Runnable接口可以避免单继承的局限性。而且,继承Thread,是可以对Thread类中的方法,进行子类复写的。但是不需要做这个复写动作的话,只为定义线程代码存放位置,实现Runnable接口更方便一些。所以Runnable接口将线程要执行的任务封装成了对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Thread(new Runnable(){  //匿名
public void run()
{
    System.out.println("runnable run");
}
})
 
{
public void run()
{
    System.out.println("subthread run");
}
 
}.start();  //结果:subthread run
1
2
3
Try {
Thread.sleep(10);
}catch(InterruptedException e){}// 当刻意让线程稍微停一下,模拟cpu 切换情况。

Thread和Runnable的区别:

如果一个类继承Thread,则不能资源共享(有可能是操作的实体不是唯一的);但是如果实现了Runable接口的话,则可以实现资源共享。

实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源
  • 可以避免java中的单继承的限制
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

设置线程优先级

1
2
3
Thread t = new Thread(myRunnable); 
t.setPriority(Thread.MAX_PRIORITY);//一共10个等级,Thread.MAX_PRIORITY表示最高级10 
t.start();<br><br>//MAX_PRIORITY : 其值是 10 <br>//MIN_PRIORITY : 其值是 1<br>//NORM_PRIORITY: 其值是 5

提示:主线程的优先级是5,不要误以为优先级越高就先执行。谁先执行还是取决于谁先去的CPU的资源

控制线程的方法:

  • join方法:假如你在A线程中调用了B线程的join方法B.join();,这时B线程继续运行,A线程停止(进入阻塞状态)。等B运行完毕A再继续运行。
  • sleep方法:线程中调用sleep方法后,本线程停止(进入阻塞状态),运行权交给其他线程。
  • yield方法:线程中调用yield方法后本线程并不停止,运行权由本线程和优先级不低于本线程的线程来抢。(不一定优先级高的能先抢到,只是优先级高的抢到的时间长)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package heimablog;
 
// 定义Runnable 接口的实现类
public class JoinTest implements Runnable {
    // 重写run() 方法
    public void run() {
        for (int i = 0; i <= 10; ++i)
            System.out.println(Thread.currentThread().getName() + "..." + i);
    }
 
    public static void main(String args[]) throws InterruptedException {
        // 创建Runnable 接口实现类的实例
        JoinTest t = new JoinTest();
        // 通过 Thread(Runnable target) 创建新线程
        Thread thread = new Thread(t);
        Thread thread1 = new Thread(t);
        // 启动线程
        thread.start();
        // 只有等thread 线程执行结束,main 线程才会向下执行;
        thread.join();
        System.out.println("thread1 线程将要启动");
        thread1.start();
    }
}
  • wait方法:当前线程转入阻塞状态,让出cpu的控制权,解除锁定。
  • notify方法:唤醒因为wait()进入阻塞状态的其中一个线程。
  • notifyAll方法: 唤醒因为wait()进入阻塞状态的所有线程。

这三个方法都必须用synchronized块来包装,而且必须是同一把锁,不然会抛出java.lang.IllegalMonitorStateException异常。

多线程安全问题的原因:

发现一个线程在执行多条语句时,并运算同一个数据时,在执行过程中,其他线程参与进来,并操作了这个数据,导致了错误数据的产生。

涉及到两个因素:

  1. 多个线程在操作共享数据。
  2. 有多条语句对共享数据进行运算。

如下面程序:

运行的结果会出现"一号窗口...卖出了第0张票,四号窗口...卖出了第-1张票,二号窗口...卖出了第-2张票"这样的安全问题。

解决安全问题的原理

只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他线程不能进来执行就可以解决这个问题。

解决这类问题的方法:

  1、同步代码块。

  2、同步函数。

同步代码块

1
2
3
4
synchronized(obj)  // 任意对象都可以,这个对象就是锁。
{
    //此处的代码就是同步代码块,也就是需要被同步的代码;
}

synchronized 后括号里面的 obj 就是同步监视器。代码含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。即只有获得对同步监视器的锁定的线程可以在同步中执行,没有锁定的线程即使获得执行权,也不能在同步代码块中执行。

注意:虽然JAVA 程序允许使用任何对象来作为同步监视器。但是还是推荐使用可能被并发访问的共享资源来充当同步监视器。

修改代码如下:

加入同步监视器之后的程序就不会出现数据上的错误了。

  虽然同步监视器的好处是解决了多线程的安全问题。但也也因为多个线程需要判断锁,较为消耗资源。

同步前提:

  1. 必须要有两个或者两个以上的线程,才需要同步。
  2. 多个线程必须保证使用的是同一个锁。

如果加入了synchronized 同步监视器,还出现了安全问题,则可以按照如下步骤找寻错误:

  1. 明确那些代码是多线程代码。
  2. 明确共享数据。
  3. 明确多线程运行代码中那些代码是操作共享数据的。

同步函数

  把 synchronized 作为修饰符修饰函数。则该函数称为同步函数。

  注意:同步函数无需显示的指定同步监视器,函数都有自己所属的对象this,同步函数的同步监视器是this,也就是该对象本身。

  注意:synchronized 关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。

上面通过模拟火车卖票系统的小程序,通过加入 synchronized 同步监视器,来解决多线程中的安全问题。下面模拟银行取钱问题,通过同步函数来解决多线程的安全问题。

同步方法的监视器是 this ,因此对于同一个 Account 而言,任意时刻只能有一条线程获得 Account 对象的锁定。

提示:可变类的线程安全是以降低运行程序的运行效率作为代价,为了减少线程安全所带来的负面影响,程序可以采用如下策略:

  • 只对会改变竞争资源的方法进行同步。
  • 在两个或两个以上的线程操作同一个锁的环境中使用同步。

当如下情况发生时会释放同步监视器的锁定:

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程的同步方法、同步代码块中遇到break 、 return终止了该代码块、该方法的继续执行。
  • 当前线程的同步方法、同步代码块出现了未处理的Error或Exception,导致该代码块、该方法异常结束时会释放同步锁。
  • 当线程执行同步方法、同步代码块,程序执行了同步监视器对象的wait() 方法时。

当同步函数被static修饰时,这时的同步用的是哪个锁呢?

静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,这个对象就是该类的字节码文件对象。

所以静态加载时,只有一个对象存在,那么静态同步函数就使用的这个对象。这个对象就是 类名.class

同步代码块和同步函数的区别?

  • 同步代码块使用的锁可以是任意对象。
  • 同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。

在一个类中只有一个同步,可以使用同步函数。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些

死锁

  当两线程相互等待对方释放锁时,就会发生死锁。由于JVM没有监测,也没有采用措施来处理死锁,所以多线程编成时应该采取措施来避免死锁。

单例模式之懒汉式

懒汉式:延迟加载方式。

当多线程访问懒汉式时,因为懒汉式的方法内对共性数据进行多条语句的操作,所以容易出现线程安全问题。

  • 为了解决安全问题,加入同步机制。但是却带来了效率降低。
  • 为了解决效率问题,通过双重判断的形式解决。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Single {
 
    private static Single s = null;
     private Single() {
    <br>     }
 
    public static Single getInstance() {
        if (s == null) {
            synchronized (Single.class) {//用字节码文件对象作为锁;
                if (s == null)
                    s = new Single();
            }
        }
        return s;
    }
}

同步死锁:通常只要将同步进行嵌套,就可以看到现象。

线程通信

思路:多个线程在操作同一个资源,但是操作的动作却不一样。

  1. 将资源封装成对象。
  2. 将线程执行的任务(任务其实就是run方法。)也封装成对象。

模拟生产消费者:系统在有两条线程,分别代表生成者和消费者。

  程序的基本流程:

  1. 生成者生产出一件商品。
  2. 消费者消费生成出的商品。

  通过上诉流程要了解:生成者和消费者不能连续生成或消费商品,同时消费者只能消费已经生产出的商品。

上面的程序中:flag 标志位 是判断 是由生产者生成还是由消费者进行消费。其实,在现实生活中,不可能只有一个生成者和消费者,而是多个生成者和消费者。所以用 while 循环来进行 flag 的判断 , 而不是用 if 。如果用if 容易出现线程安全问题;而且在用while 循环进行flag的判断时,则必须用 notifyAll() 方法来唤醒同步监视器中所有等待中的线程,而不是 用notify() 方法。用notify()  则会导致所有线程进入等待状态。

上面的小程序借助Object 类提供的 wait()、notify()、notifyAll 三个方法【等待唤醒机制涉及的方法】

  • wait() :导致当前线程等待,知道其他线程调用该同步监视器的notify()或notifyAll() 方法来唤醒线程。
  • notify() : 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择一个其中一个唤醒。
  • notifyAll() :唤醒此同步监视器上等待的所有单个线程。

注意

  • 这三个方法必须用同步监视器对象来调用:
    1. 同步函数:因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用。
    2. 同步代码块:必须使用 synchronized 括号中的对象来调用。
  • 这些方法都需要定义在同步中:因为这些方法必须要标示所属的锁。你要知道 A锁上的线程被wait了,那这个线程就相当于处于A锁的线程池中,只能A锁的notify唤醒。
  • 这三个方法都定义在Object类中。为什么操作线程的方法定义在Object类中?因为这三个方法都需要定义同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在Object类中。

wait和sleep区别

分析这两个方法从执行权和锁上来分析:

wait:可以指定时间也可以不指定时间。不指定时间,只能由对应的notify或者notifyAll来唤醒。

sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。

wait:线程会释放执行权,而且线程会释放锁。

Sleep:线程会释放执行权,但不是不释放锁。

线程的停止【stop方法已过时】

原理:让线程运行的代码结束,也就是结束run方法。

怎么结束run方法?一般run方法里肯定定义循环。所以只要结束循环即可。

  • 第一种方式:定义循环的结束标记。
  • 第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过Thread类中的interrupt方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。

Thread's functions

  • interrupt():中断线程。
  • setPriority(int newPriority):更改线程的优先级。
  • getPriority():返回线程的优先级。
  • toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
  • Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。
  • setDaemon(true):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
  • join:临时加入一个线程的时候可以使用join方法。当A线程执行到了B线程的join方式。A线程处于冻结状态,释放了执行权,B开始执行。A什么时候执行呢?只有当B线程运行结束后,A才从冻结状态恢复运行状态执行。

同步锁LOCK

  JDK 1.5之后,JAVA提供了另外一种线程同步机制:显示定义同步锁来实现同步,解决线程安全问题使用同步的形式,(同步代码块,要么同步函数)其实最终使用的都是锁机制。同步锁应该使用Lock对象充当。在面向对象中谁拥有数据谁就对外提供操作这些数据的方法 ,发现获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class X {
    // 定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
 
    // 定义需要保证线程安全的方法
    public void m() {
        // 加锁
        lock.lock();
        try {
            // 需要保证线程安全的代码
        } finally {
            lock.unlock();
        }
    }
}

所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。

现在锁是指定对象Lock。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装,并提供了功能一致的方法 await()、signal()、signalAll().

Condition接口:await()、signal()、signalAll();

Condition 实例实质上被绑定在一个Lock 对象上。如:

1
2
3
4
//定义锁对象
private final ReentrantLock lock = new ReentrantLock() ;
//指定Lock 对象对应的条件变量
private final Condition condition = lock.newCondition() ;
  • await() : 类似 wait() 方法。
  • signal() : 类似 notify() 方法。
  • signalAll() : 类似 notifyAll() 方法。
posted @   iadanac  阅读(361)  评论(0编辑  收藏  举报
编辑推荐:
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
阅读排行:
· 趁着过年的时候手搓了一个低代码框架
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· 乌龟冬眠箱湿度监控系统和AI辅助建议功能的实现
点击右上角即可分享
微信分享提示