Java并发编程学习系列一:线程与锁(一)

简介: Java并发编程学习系列一:线程与锁(一)

概念

什么是线程和进程?


进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同进程下的线程共享进程的方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。


进程和线程的区别是什么?


  • 进程是运行中的程序,线程是进程的内部的一个执行序列;
  • 进程是资源分配的单元,线程是执行行单元;
  • 进程间切换代价大,线程间切换代价小;
  • 进程拥有资源多,线程拥有资源少;
  • 地址空间和其它资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见;
  • 通信:进程间通信 IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性;
  • 在多线程 OS 中,进程不是一个可执行的实体;


创建线程有几种不同的方式?你喜欢哪一种?为什么?


  1. 继承 Thread 类(真正意义上的线程类),重写 run 方法,其中 Thread 是 Runnable 接口的实现。
  2. 实现 Runnable 接口,并重写里面的 run 方法。
  3. 使用 Executor 框架创建线程池。Executor 框架是 juc 里提供的线程池的实现。
  4. 实现 callable 接口,重写 call 方法,有返回值。

一般情况下使用 Runnable 接口,避免单继承的局限,一个类可以继承多个接口;适合于资源的共享。

注意:Java 自己开启不了线程,在 Thread 类中执行 start 方法时,本质上调用的是本地方法 start0,即执行底层的 C++代码,Java 无法直接操作硬件。


概括的解释下线程的几种可用状态。


  1. 新建( new ):新创建了一个线程对象。


  1. 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。


  1. 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。


  1. 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:


  • 等待阻塞:运行( running )的线程执行 o.wait ()方法, JVM 会把该线程放入等待队列( waitting queue )中。
  • 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
  • 其他阻塞: 运行( running )的线程执行 Thread. sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。


  1. 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。


image.png


并行和并发有什么区别?


  • 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  • 在一台处理器上“同时”处理多个任务指的是并发,在多台处理器上同时处理多个任务指的是并行。如 hadoop 分布式集群。
    并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。


首先给出结论:“并行”概念是“并发”概念的一个子集。我们经常听说这样一个关键词“多线程并发编程”,一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。


如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。


如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。

推荐阅读:并发与并行的区别? - Limbo的回答 - 知乎


runnable 和 callable 有什么区别?


  • 实现 Callable 接口的任务线程能返回执行结果;而实现 Runnable 接口的任务线程不能返回结果;
  • Callable 接口的 call()方法允许抛出异常;而 Runnable 接口的 run()方法的异常只能在内部消化,不能继续上抛;


Lock锁


传统 synchronized


/**
 * 真正的多线程并发,公司中的开发,降低耦合性
 * 线程就是一个单独的资源类,没有任何附属的操作!
 * 1.属性、方法
 */
public class SychronizedDemo {
    public static void main(String[] args) {
        //并发,多线程操作同一个资源类,把资源类丢入线程
        Ticket ticket = new Ticket();
        //声明线程使用lambda表达式,简化匿名内部类的书写
        new Thread(()->{
            for(int i=0;i<20;i++){
                ticket.sale();
            }
        },"A").start();
        new Thread(()->{
            for(int i=0;i<20;i++){
                ticket.sale();
            }
        },"B").start();
        new Thread(()->{
            for(int i=0;i<20;i++){
                ticket.sale();
            }
        },"C").start();
    }
}
class Ticket{
    private int num = 30;
    public synchronized void sale(){
        if (num > 0){
            System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余:"+(--num));
        }
    }
}
复制代码


执行结果为:


A卖出了一张票,剩余:29
A卖出了一张票,剩余:28
A卖出了一张票,剩余:27
A卖出了一张票,剩余:26
A卖出了一张票,剩余:25
B卖出了一张票,剩余:24
B卖出了一张票,剩余:23
B卖出了一张票,剩余:22
B卖出了一张票,剩余:21
B卖出了一张票,剩余:20
B卖出了一张票,剩余:19
B卖出了一张票,剩余:18
B卖出了一张票,剩余:17
B卖出了一张票,剩余:16
B卖出了一张票,剩余:15
B卖出了一张票,剩余:14
B卖出了一张票,剩余:13
B卖出了一张票,剩余:12
B卖出了一张票,剩余:11
B卖出了一张票,剩余:10
B卖出了一张票,剩余:9
B卖出了一张票,剩余:8
B卖出了一张票,剩余:7
B卖出了一张票,剩余:6
B卖出了一张票,剩余:5
A卖出了一张票,剩余:4
A卖出了一张票,剩余:3
A卖出了一张票,剩余:2
A卖出了一张票,剩余:1
A卖出了一张票,剩余:0
复制代码


上述代码讲述的是卖票的例子,总共有30张票,现在交由3个售票员进行售票,每次只能允许一个售票员来进行售票行为,共用同一个票源。按照这样的需求,我们首先想到的是使用 synchronized 关键字,synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。


Lock锁


查看Lock锁的使用模版:


image.png


我们还是基于上述代码进行调整,主要就修改 Ticket 对象方法。


class Ticket{
    private int num = 30;
    Lock lock = new ReentrantLock();
    public void sale(){
        lock.lock();
        try {
            if (num > 0){
                System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余:"+(--num));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
复制代码


执行结果一致。


使用 Lock 锁三步曲,1、声明锁;2、加锁;3解锁。


说说 synchronized 关键字和 Lock 类的区别


  • synchronized 是 Java 内置的关键字,Lock 是一个类;
  • synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁;
  • synchronized 会自动释放锁,Lock 必须手动释放锁,如果不释放,将会造成死锁;
  • synchronized 如果有多个线程,线程1获得锁执行时,其他线程只能傻傻的等待,Lock 锁不一定要等下去
  • synchronized 是可重入锁,不可中断,非公平锁,Lock 可重入锁,可以判断锁,非公平锁(可以设置,自由度更高);
  • synchronized 适合锁少量代码的同步问题,Lock 适合锁大量的代码。


public ReentrantLock() {
        this.sync = new ReentrantLock.NonfairSync();
    }
    public ReentrantLock(boolean var1) {
        this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
    }
复制代码


ReadWriteLock


1.jpg


该接口允许多个线程同时做读操作,但是每次只能有一个线程来做写操作。


public class ReadWriteLockDemo {
    public static void main(String[] args) {
        Mycache mycache = new Mycache();
        for (int i = 0; i < 5; i++) {
            final int index = i;
            new Thread(()->{
                mycache.put("hresh"+index);
            },String.valueOf(i)).start();
        }
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                mycache.get();
            },String.valueOf(i)).start();
        }
    }
}
class Mycache{
    private volatile Map<String,Object> map = new HashMap<>();
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    public void put(String data){
        reentrantReadWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"准备写入操作");
            map.put("name",data);
            System.out.println(Thread.currentThread().getName()+"写入成功"+data);
        } finally {
            reentrantReadWriteLock.writeLock().unlock();
        }
    }
    public void get(){
        reentrantReadWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"读取操作");
            System.out.println(map.get("name"));
            System.out.println(Thread.currentThread().getName()+"读取成功");
        } finally {
            reentrantReadWriteLock.readLock().unlock();
        }
    }
}
复制代码
0准备写入操作
0写入成功hresh0
2准备写入操作
2写入成功hresh2
3准备写入操作
3写入成功hresh3
1准备写入操作
1写入成功hresh1
4准备写入操作
4写入成功hresh4
2读取操作
hresh4
2读取成功
0读取操作
1读取操作
hresh4
1读取成功
hresh4
0读取成功
3读取操作
hresh4
3读取成功
4读取操作
hresh4
4读取成功
复制代码


如果将 ReentrantReadWriteLock 改为 ReentrantLock 实现,观察代码运行结果,可以发现使用了 ReentrantLock 的代码,每次只能有一个线程做读操作,而 ReentrantReadWriteLock 则是共享锁,可以允许多个线程来做读操作。且读写操作互斥,必须写完之后才能读取。


Callable


1.jpg


注意:Callable 接口支持返回执行结果,此时需要调用 FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableImpl implements Callable<String> {
    private String acceptStr;
    public CallableImpl(String acceptStr){
        this.acceptStr = acceptStr;
    }
    @Override
    public String call() throws Exception {
//        int i = 1/0;
        Thread.sleep(3000);
        System.out.println("hello : " + this.acceptStr);
        return this.acceptStr + " append some chars and return it!";
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new CallableImpl("my callable test!");
        FutureTask<String> task = new FutureTask<>(callable);
        long startTime = System.currentTimeMillis();
        //创建线程
        new Thread(task).start();
        // 调用get()阻塞主线程,反之,线程不会阻塞
        String result = task.get();
        long endTime = System.currentTimeMillis();
        System.out.println("hello : " + result);
        System.out.println("cast : " + (endTime - startTime) / 1000 + " second!");
    }
}
复制代码


结果为:


//执行结果为:
hello : my callable test!
hello : my callable test! append some chars and return it!
cast : 3 second!
//如果注释get()方法,结果变为:
cast : 0 second!
hello : my callable test!
复制代码


当取消 call 方法中关于 int i = 1/0;的注释,程序结果变为:


2.jpg


从结果中可以看出,异常信息会向上抛出。



目录
相关文章
|
26天前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
122 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
1月前
|
Java 调度 开发者
Java线程池ExecutorService学习和使用
通过学习和使用Java中的 `ExecutorService`,可以显著提升并发编程的效率和代码的可维护性。合理配置线程池参数,结合实际应用场景,可以实现高效、可靠的并发处理。希望本文提供的示例和思路能够帮助开发者深入理解并应用 `ExecutorService`,实现更高效的并发程序。
36 10
|
1月前
|
Java 数据库连接 数据库
【潜意识Java】深度分析黑马项目《苍穹外卖》在Java学习中的重要性
《苍穹外卖》项目对Java学习至关重要。它涵盖了用户管理、商品查询、订单处理等模块,涉及Spring Boot、MyBatis、Redis等技术栈。
118 4
|
1月前
|
前端开发 Java 数据库连接
【潜意识Java】深度解读JavaWeb开发在Java学习中的重要性
深度解读JavaWeb开发在Java学习中的重要性
38 4
|
1月前
|
存储 移动开发 算法
【潜意识Java】Java基础教程:从零开始的学习之旅
本文介绍了 Java 编程语言的基础知识,涵盖从简介、程序结构到面向对象编程的核心概念。首先,Java 是一种高级、跨平台的面向对象语言,支持“一次编写,到处运行”。接着,文章详细讲解了 Java 程序的基本结构,包括包声明、导入语句、类声明和 main 方法。随后,深入探讨了基础语法,如数据类型、变量、控制结构、方法和数组。此外,还介绍了面向对象编程的关键概念,例如类与对象、继承和多态。最后,针对常见的编程错误提供了调试技巧,并总结了学习 Java 的重要性和方法。适合初学者逐步掌握 Java 编程。
56 1
|
2月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
2月前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
16天前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
41 17
|
25天前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
53 26
|
2月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
239 2

热门文章

最新文章