Java从入门到精通22==》程序、进程、线程、死锁问题

Java从入门到精通22==》程序、进程、线程

2023.8.15

文章目录

  • Java从入门到精通22==》程序、进程、线程
    • 一、程序、进程、线程
      • 1、程序概述
      • 2、进程概述
      • 3、线程概述
    • 二、线程的创建(实现多线程)
      • 1、继承Thread类
        • 1.基本方法
        • 2.步骤
      • 2、实现Runnable接口
        • 1.基本方法
        • 2.步骤
      • 3、两种实现方式对比
        • 1.继承Thread类的优点:
        • 2.继承Thread类的缺点:
        • 3.实现Runnable接口的优点:
        • 4. 实现Runnable接口的缺点:
    • 三、线程的常用方法
      • 1、获取当前线程、名称和设置名称
        • 方法
      • 2、线程休眠
        • 方法
      • 3.线程优先级
        • 方法
      • 4.守护线程
        • 方法
      • 5.加入线程
        • 方法
      • 6.礼让线程
        • 方法
    • 四、数据共享
      • 如何实现
    • 五、线程安全
      • 1、原因
      • 2、Java如何保证线程安全
        • 1.synchronized关键字
        • 2、Lock接口
    • 六、死锁

一、程序、进程、线程

1、程序概述

程序是一组计算机指令的有序集合,这些指令告诉计算机如何执行特定的任务

2、进程概述

进程是指正在运行中的程序实例。一个程序被执行时,系统会为该程序创建一个进程,进程包括该程序的代码、数据、堆栈等信息。每个进程都有自己的独立内存空间,它与其他进程的内存空间是隔离的。在操作系统中,进程是调度和分配资源的基本单位。

3、线程概述

线程是计算机程序中的一个执行序列。它是进程中的一个单独的控制流,独立地运行于其他线程之上。线程共享进程的资源,如内存、文件句柄、网络连接等,但每个线程有自己的程序计数器、堆栈和局部变量。多线程可以提高程序的并发性和效率,因为它们可以同时执行多项任务。

二、线程的创建(实现多线程)

1、继承Thread类

1.基本方法

void run()
	在线程开启后,此方法将被调用执行
void start()
	使此线程开始执行,Java虚拟机会调用run方法()

2.步骤

  1. 定义一个类MyThread继承Thread类

  2. 在MyThread类中重写run()方法

package 线程的创建;

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("我是线程!");
    }
}

  1. 创建MyThread类的对象
  MyThread myThread = new MyThread();
  1. 调用线程对象start方法,启动线程
  myThread.start();

Java从入门到精通22==》程序、进程、线程、死锁问题_第1张图片

注意: 在MyThread类中重写run()方法是为了实现自定义的线程执行逻辑。因为每个线程都有自己的run()方法,所以可以根据实际需求重写这个方法,从而使每个线程执行不同的任务。
简单地说,run()是用来封装被线程执行的代码

2、实现Runnable接口

1.基本方法

Thread(Runnable target)
	分配一个新的Thread对象
Thread(Runnable target, String name)
	分配一个新的Thread对象

2.步骤

  1. 定义一个类MyRunnable实现Runnable接口

  2. 在MyRunnable类中重写run()方法

package 线程的创建;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("我是线程!");
    }
}

  1. 创建MyRunnable类的对象
MyRunnable myRunnable = new MyRunnable();
  1. 创建Thread类的对象,把MyRunnable对象作为构造方法的参数调用Thread类含参构造器
Thread thread = new Thread(myRunnable);
  1. 调用Thread类的start方法,启动线程
thread.start();

Java从入门到精通22==》程序、进程、线程、死锁问题_第2张图片

3、两种实现方式对比

1.继承Thread类的优点:

  1. 简单易用:直接继承Thread类,重写run()方法即可,代码量相对较少,易于理解和维护。
  2. 对线程的控制性强:因为Thread类是可以直接被控制的,可以通过start()、stop()、suspend()等方法来控制线程的启动、停止和暂停等操作。

2.继承Thread类的缺点:

  1. 难以扩展:由于Java只支持单继承,因此如果继承了Thread类,就无法再继承其他类,这就限制了程序的扩展性。
  2. 资源消耗多:因为Thread类本身就是一个顶级对象,所以如果每个线程都是通过继承Thread类来创建的,那么就会消耗大量的系统资源。

3.实现Runnable接口的优点:

  1. 可以避免单继承的限制:因为Java支持实现多个接口,可以通过实现Runnable接口来避开单继承的限制。
  2. 资源消耗较少:因为每个线程都是通过实现Runnable接口来创建的,所以不会像继承Thread类那样消耗大量的系统资源。

4. 实现Runnable接口的缺点:

  1. 对线程的控制性弱:由于Runnable接口并没有提供像Thread类那样的控制方法,所以无法通过start()、stop()、suspend()(已弃用)等方法来控制线程的启动、停止和暂停等操作。这就需要通过某些其他方式来实现对线程的控制。
  2. 代码稍微复杂一点:相对于继承Thread类来说,实现Runnable接口的代码可能略微复杂一些,因为需要手动创建一个Thread对象并将Runnable对象传递给它。

三、线程的常用方法

1、获取当前线程、名称和设置名称

方法

	void  setName(String name)
		将此线程的名称更改为等于参数name
	String  getName()
		返回此线程的名称
	Thread  currentThread()
		返回对当前正在执行的线程对象的引用

代码示例:

package 获取当前线程名称和设置名称;

public class Demo {
    public static void main(String[] args) {
        Thread t1=Thread.currentThread();
        System.out.println(t1);
        System.out.println(t1.getName());
        t1.setName("lll");
        System.out.println(t1.getName());
    }
}

Java从入门到精通22==》程序、进程、线程、死锁问题_第3张图片

2、线程休眠

方法

	static void sleep(long millis)
		使当前正在执行的线程停留(暂停执行)指定的毫秒数

代码示例:

package 线程休眠;

public class Demo {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for (int i=0;i<10;i++){
                    try {
                        Thread.sleep(1000);
                        System.out.println(i);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }.start();
    }
}

Java从入门到精通22==》程序、进程、线程、死锁问题_第4张图片

3.线程优先级

Java中的线程优先级是一个整数值,是一个从1到10之间的数字,其中1表示最低的优先级,10表示最高的优先级。Java中线程优先级的概念是用来帮助操作系统在调度线程时做出决策的。默认情况下,所有线程的优先级都是5。
需要注意的是,线程优先级的设置只是给操作系统一个提示,而不是一个指令。操作系统可能不会完全遵循这个提示,并根据其他因素(如线程的状态、负载平衡等)来调度线程。因此,我们不应该过度依赖线程优先级来保证线程的执行顺序和效率。(简单地说,线程优先级越大,被执行可能性越大)

方法

	final int getPriority()
		返回此线程的优先级
	final void setPriority(int newPriority)
		更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10

代码示例:

package 线程优先级;
public class Demo {
    public static void main(String[] args) {
      Thread t1=  new Thread(){
            @Override
            public void run() {
                for (int i=0;i<1000;i++) {
                    System.out.println("t1线程"+i);
                }
            }
        };
        Thread t2=  new Thread(){
            @Override
            public void run() {
                for (int i=0;i<1000;i++) {
                    System.out.println("t2线程"+i);
                }
            }
        };
        System.out.println(t1.getPriority());
        System.out.println(t2.getPriority());
        t1.setPriority(10);
        t2.setPriority(1);
        t1.start();
        t2.start();
    }
}

Java从入门到精通22==》程序、进程、线程、死锁问题_第5张图片
默认是5,将t1线程优先级设为10,t2线程设为1后,系统会优先概率先进行t1线程,可以自行运行看看。

4.守护线程

方法

	void setDaemon(boolean on)
		将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

示例代码:

package 线程优先级;
public class Demo {
    public static void main(String[] args) {
        Thread t1=  new Thread(){
            @Override
            public void run() {
                for (int i=0;i<100;i++) {
                    System.out.println("t1线程"+i);
                }
            }
        };
        Thread t2=  new Thread(){
            @Override
            public void run() {
                for (int i=0;i<1000;i++) {
                    System.out.println("t2线程"+i);
                }
            }
        };
        t2.setDaemon(true);
        t1.start();
        t2.start();
    }
}

Java从入门到精通22==》程序、进程、线程、死锁问题_第6张图片
t2线程迭代到89,因为t1线程结束,t2线程也跟随着结束

5.加入线程

方法

	final void join()
		等待这个线程死亡。
			final void join(long millis)
	final void join(long millis)
		等待这个线程死亡的时间最多为millis毫秒。 0的超时意味着永远等待。
	final void join(long millis, int nanos)
		等待最多millis毫秒加上这个线程死亡的nanos纳秒
package 加入线程;

public class Demo {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i=0;i<100;i++) {
                    System.out.println("t1线程"+i);
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i=0;i<100;i++) {
                    if (i==10){
                        try {
                            t1.join();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    System.out.println("t2线程"+i);
                }
            }
        };
        t1.start();
        t2.start();
    }
}

Java从入门到精通22==》程序、进程、线程、死锁问题_第7张图片
当t2线程里面i到10时,t1线程加入,t2线程就暂时不走了,知道t1线程走完才继续走t2线程

6.礼让线程

使用礼让线程可以使多个线程之间更好地协调和共享CPU资源

方法

	 static void yield()
		向调度程序提示当前线程愿意放弃其当前使用的处理器。
		调度程序可以自由地忽略此提示。
package 礼让线程;

public class Demo {
    public static void main(String[] args) {
        Thread t1=  new Thread(){
            @Override
            public void run() {
                for (int i=0;i<100;i++) {
                    System.out.println("t1线程"+i);
                    Thread.yield();
                }
            }
        };
        Thread t2=  new Thread(){
            @Override
            public void run() {
                for (int i=0;i<100;i++) {
                    System.out.println("t2线程"+i);
                    Thread.yield();
                }
            }
        };
        t1.start();
        t2.start();
    }
}


四、数据共享

如何实现

Java线程可以通过Runnable接口实现数据共享。具体来说,可以将数据定义在Runnable实现类中,并将该实现类作为参数创建Thread对象。在多个线程中,可以共享Runnable实现类中的数据,从而实现数据共享。
示例代码:

package 数据共享;
public class MyRunnable implements Runnable{
    int ticket = 100;
    @Override
    public void run() {
        while (true){
            if (ticket>0){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+":"+ticket--);
            }
        }
    }
}

package 数据共享;

public class Demo {
    public static void main(String[] args) {
        MyRunnable myThread = new MyRunnable();
        Thread t1=new Thread(myThread,"大麦");
        Thread t2=new Thread(myThread,"小麦");
        Thread t3=new Thread(myThread,"中麦");
        t1.start();
        t2.start();
        t3.start();
    }
}

Java从入门到精通22==》程序、进程、线程、死锁问题_第8张图片

要注意的是,多个线程同时访问共享变量会存在线程安全的问题,需要进行加锁等操作来保证线程安全

五、线程安全

1、原因

当数据共享时,每条线程都可以对数据进行读取和修改,实现数据共享的同时伴随而来的是数据安全

2、Java如何保证线程安全

1.synchronized关键字

使用synchronized关键字可以让方法或者代码块在同一时刻只能被一个线程访问,从而保证线程安全。

  1. 对象锁

使用synchronized关键字可以对对象进行加锁,使得在同一时刻只有一个线程能够访问该对象的方法或者代码块。例如:

public synchronized void method() {
    // 这里的代码在同一时刻只能被一个线程执行
}
  1. 类锁

使用synchronized关键字可以对类进行加锁,使得在同一时刻只有一个线程能够访问该类的静态方法或者静态代码块。例如:

public static synchronized void method() {
    // 这里的代码在同一时刻只能被一个线程执行
}
  1. 代码块锁

除了对整个方法或类进行加锁,还可以使用synchronized关键字对代码块进行加锁,例如:

public void method() {
    synchronized (this) {
        // 这里的代码在同一时刻只能被一个线程执行
    }
}
  1. 静态代码块锁

使用synchronized关键字对静态代码块进行加锁,该锁对该类的所有对象都有效。例如:

public class MyClass {
    static {
        synchronized (MyClass.class) {
            // 这里的代码在同一时刻只能被一个线程执行
        }
    }
}
package 数据共享;
public class MyRunnable implements Runnable{
    int ticket = 100;
    @Override
    public synchronized void run() {
        while (true){
            if (ticket>0){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+":"+ticket--);
            }
        }
    }
}
package 数据共享;

public class Demo {
    public static void main(String[] args) {
        MyRunnable myThread = new MyRunnable();
        Thread t1=new Thread(myThread,"大麦");
        Thread t2=new Thread(myThread,"小麦");
        Thread t3=new Thread(myThread,"中麦");
        t1.start();
        t2.start();
        t3.start();
    }
}

需要注意的是,使用synchronized关键字锁定的对象应该是多线程共享的资源,否则会浪费锁的性能。同时,在使用synchronized关键字的时候,也需要考虑是否会造成死锁等问题

2、Lock接口

使用Lock接口可替代synchronized关键字,Lock接口可以更加灵活地控制锁的粒度,从而提高并发性能。
步骤:

  1. 创建一个Lock对象。Lock对象可以通过Java的Lock类的静态方法newReentrantLock()来创建。
  2. 在需要进行线程同步的代码块中,使用lock()方法进行锁定。
  3. 在锁定的代码块中,使用try-finally语句块来确保最终释放锁定。
package 数据共享;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable{
    int ticket = 100;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        lock.lock();
        try {
            while (true){
                if (ticket>0){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName()+":"+ticket--);
                }
            }
        } finally {
            lock.unlock();
        }
    }
}

另外,在使用时需要注意锁定的作用域和锁定的粒度,以及可能出现的死锁和饥饿问题。

六、死锁

Java死锁是指两个或多个线程无限期地相互等待对方所持有的资源而无法继续执行。在死锁状态下,每个线程都在等待其他线程释放它所需的资源,从而形成了一种循环依赖,导致程序无法继续执行下去。
代码示例:

package 死锁;

public class Demo {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1=  new Thread(){
            @Override
            public void run() {
                synchronized (lock1) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock2) {
                        System.out.println("我是t1线程");
                    }
                }
            }
        };
        Thread t2=  new Thread(){
            @Override
            public void run() {
                synchronized (lock2) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock1) {
                        System.out.println("我是t2线程");
                    }
                }
            }
        };
    t1.start();
    t2.start();
    }
}

线程1先获取了lock1,然后尝试获取lock2,而线程2先获取了lock2,然后尝试获取lock1。这样,两个线程都拥有了一个锁,然后都在等待对方释放另一个锁,导致死锁。

你可能感兴趣的:(Java从入门到精通,java,开发语言,深度学习)