Java小白教学—五千字带你了解多线程机制及线程安全问题

简介: Java小白教学—五千字带你了解多线程机制及线程安全问题

基础概念


📖 问题一 : 什么是线程?线程和程序、进程有什么区别?

程序:为实现某种功能,使用计算机语言编写的一系列指令的集合。

          指的是静态的代码(例如安装在电脑上的那些文件)

进程:是运行中的程序(如运行中的王者荣耀)进程是操作系统进行资源分配的最小单位。

线程:进程可以进一步细化为线程,是进程中一个最小的执行单元,是cpu进行调度的最小单元

          例如:QQ中的一个聊天窗口

进程和线程的关系:

•  一个进程中可以包含多个线程. (一个QQ程序可以有多个聊天窗口)


•  一个线程只能隶属于一个进程. (QQ的聊天窗口只能属于QQ进程)


•  每一个进程至少包含一个线程,也就是我们的主线程(像java中的main方法就是来启动主线程的)在主线程中可以创建并启动其他线程.


•  一个进程的线程共享该进程的内存资源.

📖 问题二 : 什么是多线程?多线程有哪些优缺点?


 ✎. 顾名思义多线程指:在一个程序中可以创建多个线程执行.


【优点】 提高程序执行效率(多个任务可以在不同的线程中同时执行)


              提高了cpu的利用率


              改善程序结构,将复杂任务拆分成若干个小任务


【缺点】 线程也是程序,线程越多占用内存也越多,cpu开销变大(可扩充内存或升级cpu)


          ✰  线程之间同时对共享资源的访问会相互影响,若不加以控制会导致数据出错.


 📖 问题三: 如何解决多线程操作共享数据的问题?

✎. 多个线程同时访问操作同一个共享的数据( 例如买票、抢购等 )时,可能会引起冲突,所以引入


线程 “同步” 机制,即各线程间要有先来后到。


即通过 【 排队+锁 】 在关键的步骤处,使多个线程只能一个一个的执行.


那么问题又来了,什么是锁呢?

锁机制(Lock)


📖 问题四: 什么是锁?锁有什么用?锁怎么用?

✎. 关于synchronized ( 同步锁 )  

语法结构:

 synchronized(同步锁对象) {

      同步代码块      

}  1

同步锁对象作用:


用来记录有没有线程进入到同步代码块,如果有线程进入同步代码块,那么其他线程就不能进入同步代码块,直到上一个线程执行完同步代码块的内容,释放锁之后,其他线程才能进入。


同步锁对象要求:   同步锁对象必须是唯一 的。


✎. synchronized还可修饰方法.  


    synchronized修饰方法时,同步锁对象不需要我们指定,同步锁对象会默认提供:


  •    非静态方法 ------  默认是this
  •    静态方法   ------  锁对象是当前类的class对象 (一个类的对象只有一个)

✎. 关于Lock锁


ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,可以显式加锁释放锁.


✎. ReentrantLock与synchronized区别?


  • synchronized是一个关键字 ,控制依靠底层编译后的指令去实现.
  • synchronized可以修饰一个方法 或一个代码块.
  • synchronized是 隐式 的加锁和释放锁,一旦方法或代码块出现异常,会自动释放锁.

  • ReentrantLock是一个 ,依靠java底层代码去控制 (底层有一个同步队列)
  • ReentrantLock只能修饰 代码块.
  • ReentrantLock需要 手动 的加锁和释放锁, 所以释放锁最好写在finally中 , 一旦出现异常, 保证锁能释放.

误区:不是只要有线程就需要加锁,只有多个线程对同一资源共享时才加锁

创建线程的方式



📖 问题五: 如何创建线程?有几种方式?

创建线程的方式通常有三种:

       •  通过继承Thread来创建线程

       •  通过实现Runnable接口来创建线程

       •  通过实现Callable接口来创建线程

📌 通过继承Thread来创建线程                                                                                                        


• 写一个类继承 java.lang.Thread


• 重写run( )

• 线程中要执行的任务都要写在run( )中,或在run( )中进行调用.

public class MyThread extends Thread{//继承Thread类
    @Override
    public void run() {//重写run方法
        for (int i = 1; i <= 200; i++) {
            System.out.println("run"+i);
        }
    }
}
 public static void main(String[] args) {
        //创建线程
        MyThread mythread = new MyThread();
        //启动线程
        mythread.start();
        for (int i = 1; i <= 200; i++) {
            System.out.println("main"+i);
        }
    }

注意:

    启动线程调用的是start() ; 不是run()

     run()这不是启动线程,只是一个方法调用,没有启动线程,还是单线程模式的。

📖 Thread类中的方法:

run()    用来定义线程要执行的任务代码.

start()  启动线程

currentThread() 获取到当前线程(.得到具体信息)

setName()   为线程设置名字

getState()  获取状态

getPriority() setPriority 获取/设置 优先级

sleep() 让当前线程休眠指定时间.

join()  等待当前线程执行完毕,其他线程再执行.

yield() 主动礼让,退出cpu重新回到等待序列.

📖 关于优先级:

【java中默认优先级为5, 设置优先级范围为1~10】 ( 作用:为操作系统调度算法提供的 )


📌 通过实现Runnable接口来创建线程

• 创建一个类,实现Runnable接口(即只先创建线程要执行的任务)

• 重写任务执行的Run()

• 创建线程,并为线程指定执行任务.

public class MyThread implements Runnable {//实现Runnable接口
    @Override
    public void run() {//重写run方法
        for (int i = 0; i < 200; i++) {
            System.out.println("自定义线程");
        }
    }
}
public static void main(String[] args) {
        //创建任务
        MyThread mythread = new MyThread();
        //创建线程,并指定执行任务
        Thread thread = new Thread(mythread);
        thread.start();
    }

📖 实现Runnable接口创建的优点:

 •  因为java是单继承,一旦继承一个类就不能在继承其他类,避免单继承的局限。

 •  适合多线程来处理同一份资源时使用


📌 通过实现Callable接口来创建线程

• 相比run( )方法,可以有返回值.

• 方法可以抛出异常.

• 支持泛型的返回值.

• 需要借助FutureTask类,获取返回结果

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class CallableDemo<T> implements Callable<T> {
    @Override
    public T call() throws Exception {//可以有返回值,也可以抛出异常
        Integer n = 0;
        for (int i = 0; i < 10; i++) {
            n += i;
        }
        return (T) n;
    }
 
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableDemo<Integer> callableDemo = new CallableDemo();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callableDemo);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

这段代码是打印1~10的和,通过这个案例,相信你对实现Callable接口来创建线程已经大致了解


核心代码:


 CallableDemo<Integer> callableDemo = new CallableDemo();

       FutureTask<Integer> futureTask = new FutureTask<Integer>(callableDemo);

Thread thread = new Thread(futureTask);

       thread.start();

       System.out.println(futureTask.get());

线程生命周期


📖 问题六: 一个线程的生命周期是怎样的?

线程状态:

新建:刚刚创建了一个线程对象,并没有启动.

就绪:调用start() 后线程就进入到了就绪状态(可运行状态),进入到了操作系统的调度队列.

运行状态:获得了cpu执行权,进入到cpu执行.

阻塞状态:例如调用sleep() ,有线程调用了join(),线程中进行Scanner输入...

死亡/销毁:run()方法中的任务执行完毕了.

状态关系图:

模拟卖票案例: 两个窗口分别售票,票数为10张

public class MyThread extends Thread{//我们使用了继承Thread的方法
    static int num =10;   //票总数10,且为共享资源,要用static修饰
    static String obj = new String();//可以是任意类对象,但必须唯一。
/*  synchronized(同步锁对象) {
         同步代码块
    }                         */
 
    @Override
    public void run() {//线程要执行的代码块要写在run()中
        while (true){
            synchronized (obj){//加锁,一次只能执行一个线程
                if(num>0){
                    try {
                        Thread.sleep(800);//此处加入休眠为了让运行结果更明显,也可不加
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
                    num--;  //每抢一张票,总数num(10)就减1
                }else{
                    break;
                }
            }
        }
    }
}

在Main方法中创建线程并启动:

 
public static void main(String[] args) {
        //创建两个线程,分别对应两个窗口
        MyThread myThread1 = new MyThread();
        myThread1.setName("窗口1");//线程1
        myThread1.start();
 
        MyThread myThread2 = new MyThread();
        myThread2.setName("窗口2");//线程2
        myThread2.start();
    }

运行结果:

线程通信


📖 问题七: 什么是线程通信?怎么实现线程交替运行?

✎. 线程通信指:多个线程相互调度, 相互牵制,即线程间的相互作用.

wait( )   --- 让线程等待同时释放锁.


notify( ) --- 唤醒等待线程,必须写在同步代码块中进行,必须通过锁对象调用。


notifyAll( )--唤醒所有等待的线程.


这三个方法必须使用在同步代码块或同步方法中


✎. sleep( long time )与wait( )区别:

sleep ( ) :

  • 属于Thread类中的方法
  • sleep休眠指定时间后,会自动唤醒
  • sleep( ) 不会释放锁

wait ( ) :

  • 属于Object类中的方法,必须要有锁对象调用
  • wait后的线程必须要等待其他线程唤醒(notify或notifyAll)
  • wait( ) 自动释放锁

让我们通过一个例题来体会下线程通信吧!

两个线程交替打印1-100之间的数字

public class MyThread extends Thread{
    static int num = 1;
    static String string =new String();
    @Override
    public void run() {
        while (num<=100){
            synchronized (string){
                string.notify();
 
                System.out.println(currentThread().getName()+":"+num);
                num++;
 
                try {
                    string.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        MyThread mythread1 = new MyThread();
        mythread1.start();
        MyThread mythread2 = new MyThread();
        mythread2.start();
    }
}
相关文章
|
4天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
5天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
23 4
|
5天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
25 3
|
5天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
15 2
|
6天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
18 1
|
6天前
|
Java 开发者
深入理解Java异常处理机制
【10月更文挑战第29天】在Java的世界中,异常处理如同生活的调味品,不可或缺。它确保了程序在遇到错误时不会崩溃,而是优雅地继续运行或者给出提示。本文将带你领略异常处理的奥秘,从基础的try-catch语句到高级的自定义异常,让你在面对程序中的各种“意外”时,能够从容应对。
|
30天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程
|
15天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
13 3
|
15天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
12 2
|
15天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
27 2