Java多线程基础知识总结

本文介绍了进程与线程的基础概念,包括它们的区别、线程的创建方式及特点等,并深入探讨了线程安全问题及其解决方案。

1. 进程与线程

1.1 进程

  • 指正在运行的程序,是系统进行资源分配的基本单位。
  • 目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分。
  • 单核CPU在同一个时刻,只能运行一个进程;宏观并行、微观串行。

1.2 线程

  • 可以理解为应用程序中不同的执行路径。
  • 是CPU的基本调度单位。
  • 一个进程由一个或者多个线程组成,彼此间完成不同的工作。同时执行则称为多线程。
  • 例如:迅雷是一个进程,迅雷中的多个下载任务即是多个线程。Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。

1.3 进程与线程的区别

  • 进程是操作系统资源分配的基本单位,而线程则是CPU的基本调度单位
  • 一个程序运行后至少有一个进程
  • 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的
  • 进程之间不能共享数据段地址,但是线程之间可以。

1.4 线程的组成部分

1、CPU时间片:操作系统(OS)会为每个线程分配执行时间。

2、运行数据:

   堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。

   栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。

3、线程的逻辑代码。

1.5 线程的特点

1、线程抢占式执行:效率高、可防止单一线程长时间占用CPU

2、在单核CPU中,宏观上同时执行,微观上顺序执行。

1.6 线程优先级

1、设置线程优先级:new Thread().setPriority();

2、线程优先级为1-10。默认为5,优先级越高表示获取CPU机会越多。

1.7 守护线程

1、设置线程为守护线程: new Thread.setDaemon(true);

2、线程有两类:用户线程(前台线程)、守护线程(后台线程);创建线程时默认为前台线程。

3、如果程序中所有前台线程都执行完毕了,后台线程会自动结束。

4、垃圾回收器线程属于守护线程。

1.8 创建线程的三种方式

1、class A类继承Thread类,重写run方法,class A 直接调用start方法启动线程。

static class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("Hello MyThread!");
    }
}
public static void main(String[] args) {
    new MyThread().start();
}

2、class B 实现Runnable接口,重写run方法,通过new Thread(new class B)调用start方法启动线程。

static class MyRun implements Runnable{

    @Override
    public void run() {
        System.out.println("Hello MyRun!");
    }
}
public static void main(String[] args) {
    new Thread(new MyRun()).start();
}

3、实现Callable接口,Callable具有泛型返回值、可以声明异常。通过FutureTask类将Callable对象转为可执行任务,然后使用Thread类创建线程,将futureTask提交给线程。

public static void main(String[] args) throws Exception {
        //功能需求 使用Callable实现求1-100的和
        //1、创建Callable对象
   Callable<Integer> callable = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 0; i <= 100; i++) {
                 sum += i;
             }
             return sum;
        }
   };
   //2、把Callable对象转成可执行任务
   FutureTask<Integer> task = new FutureTask<>(callable);
   //3、创建线程
   Thread thread = new Thread(task);
   //4、启动线程
   thread.start();
   //5.获取结果(等待call指向完毕,才会返回)
   Integer sum = task.get();
   System.out.println("结果是:" + sum);
}

1.9 线程的几个基本方法

1、sleep()  : 休眠,当前线程主动休眠 millis毫秒。

static void testSleep() {
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("A" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

2、yield()  :当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。

static void testYield() {
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("A" + i);
            if (i % 10 == 0) Thread.yield();
        }
    }).start();

    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("B" + i);
            if (i % 10 == 0) Thread.yield();
        }
    }).start();
}

3、join() : 让“主线程”等待“子线程”结束之后才能继续运行。允许其他线程加入到当前线程中。加入当前线程,并阻塞当前线程,直到加入线程执行完毕。

通过此方法可以让几个线程按照特定顺序执行。

static void testJoin() {
        Thread t1=new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println("A" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2=new Thread(()->{
            try {
                t1.join();
//调用join过后,只有当t1线程运行完过后,t2线程才能开始运行,否则t1,t2是同时运行的。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("B" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
}

1.10 线程状态

通过Thread类中的State枚举得知线程有六种状态:

public enum State {
        
        NEW,

        RUNNABLE,

        BLOCKED,

        WAITING,

        TIMED_WAITING,

        TERMINATED;
}
  • NEW:当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
  • RUNNABLE:线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。
  • BLOCKED:由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
  • WAITING:线程无限期等待唤醒。
  • TIMED_WAITING:线程在等待唤醒,并且设置了等待时限。
  • TERMINATED:当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

2.线程安全问题

当多线程并发访问临界资源时,如果破坏原子操作,可能会导致数据不一致。

临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。

原子操作:不可分割的多步操作,被视为一个整体,其顺序和步骤不可打乱或缺省。

那么在应用程序中,如何保证线程的安全性?

2.1 使用同步代码块

synchronized(临界资源对象) {   //对临界资源对象加锁

   //代码 (原子操作)

}

两个线程 a,b 当线程a访问临界资源后,由于加了锁,线程b则不能再访问临界资源,当a执行完后,锁会自动释放,线程b可以访问临界资源。

对临界资源对象加锁过后,同一时刻,只能有一个线程进行原子操作,别的线程都要等待。

示例代码:

private static int index = 0;

public static void main(String[] args) throws InterruptedException {
    //创建数组
    String[] s = new String[5];
    //创建两个操作
    Runnable runnableA = new Runnable() {
        @Override
        public void run() {
            //同步代码块
            synchronized (s) {
                s[index] = "hello";
                index++;
            }
        }
    };

    Runnable runnableB = new Runnable() {
        @Override
        public void run() {
            synchronized (s) {
                s[index] = "world";
                index++;
            }
        }
    };

    //创建两个线程
    Thread threadA = new Thread(runnableA, "A");
    Thread threadB = new Thread(runnableB, "B");

    //启动线程
    threadA.start();
    threadB.start();

    //加入主线程,保证后面的打印操作执行时两个线程执行完毕
    threadA.join();
    threadB.join();

    System.out.println(Arrays.toString(s));
}

注意:synchronized (object) :不能用synchronized (new Object()),这样创建的就不是同一把锁,可以用synchronized (this) this表示当前对象,即原子操作所操作的对象。每个对象都有一个互斥锁标记,用来分配给线程的。只有拥有对象对象互斥锁标记的线程,才能进入该对象加锁的同步代码块。线程退出同步代码块时,会释放相应的互斥锁标记。

2.2 使用同步方法

synchronized 返回值类型 方法名称(形参列表0){ //对当前对象(this)加锁

  //代码(原子操作)

}

示例代码:

/**
 * @ClassName Ticket
 * @Description 票类(共享资源)
 */
public class Ticket implements Runnable{
    private int ticket=100;
    @Override
    public void run() {
       while (true){
          if (!sale()){
              break;
          }
       }
    }
    //买票(同步方法)
    private synchronized boolean sale(){  //锁---this :当前对象Ticket
        if (ticket<=0){
            return  false;
        }
        System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"张票");
        ticket--;
        return true;
    }
}



public static void main(String[] args) {
    //1、创建Ticket对象
    Ticket ticket=new Ticket();
    //2、创建线程对象
    Thread t1=new Thread(ticket,"窗口1");
    Thread t2=new Thread(ticket,"窗口2");
    Thread t3=new Thread(ticket,"窗口3");
    Thread t4=new Thread(ticket,"窗口4");
    //3、创建线程
    t1.start();
    t2.start();
    t3.start();
    t4.start();
}

注意:对方法上锁时,如果时非静态方法,锁---this 。 如果是静态方法,锁class。

2.3 同步规则

  1. 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
  2. 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。

已知JDK中线程安全的类:

1、StringBuffer  2、Vector   3、HashTable 

以上类中的公开方法,均为synchonized修饰的同步方法。

2.4 死锁

1、当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。

2、一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

例如:生活中一个男孩和一个女孩吃东西,但只有一双筷子,需要两只同时拥有才能吃东西,但男孩和女孩手中一人手中只有一只筷子,谁也不肯让谁,那么此时谁也吃不了东西,造成死锁。

代码示例:

public class MyLock {
    //两个锁  ---两根筷子
    public static Object a=new Object();
    public static Object b=new Object();
}


public class Girl extends Thread {
    @Override
    public void run() {
        synchronized (MyLock.b){
            System.out.println("女孩拿到了b");
            synchronized (MyLock.a){
                System.out.println("女孩拿到了a");
                System.out.println("女孩可以吃东西了。。。。。");
            }
        }
    }
}

public class Boy extends Thread {
    @Override
    public void run() {
        synchronized (MyLock.a){
            System.out.println("男孩拿到了a");
            synchronized (MyLock.b){
                System.out.println("男孩拿到了b");
                System.out.println("男孩可以吃东西了。。。。。");
            }
        }
    }
}

public static void main(String[] args) {
   Boy boy=new Boy();
   Girl girl=new Girl();
   girl.start();
   boy.start();
}

上述代码中,对筷子上了锁,必须要同时拿到两根筷子才能吃东西。而由于线程具有抢占式执行的特点,极有可能男孩和女孩一人抢了一根筷子,造成死锁。那么如何避免此种情况发生呢?其实很简单,代码如下:

 public static void main(String[] args) {
        Boy boy=new Boy();
        Girl girl=new Girl();
        girl.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boy.start();
 }

两个线程启动时,先启动其中一个,然后休眠一会,保证先启动那个能抢到两只筷子,然后再启动另一个线程。这样就不会造成死锁。

 

 

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值