Java多线程专题笔记上

Java编程

Java多线程专题笔记上

Java多线程专题笔记下


Java多线程专题目录


一、多线程的基本概念

1.基本概念

程序:是一段特定功能的静态代码的集合,是程序执行的蓝本。
进程:是程序的一次动态执行过程,代码的加载、执行,系统资源的调用。
线程:线程是进程内部的单一的顺序控制流程。
多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。多线程是实现并发的一种有效手段。
并发:在同一时间,多个任务交替执行。
并行:在同一时间,多个任务同时执行。

2.进程与线程的差别:

做个简单的比喻:进程=火车,线程=车厢
线程在进程下行进(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢),一个线程可以有多个协程(一个车厢有很多个乘客)
不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”。

二、创建线程的两种方式

1.第一种方法:继承Thread超类,重写run方法

class ThreadDem01 {
    public static void main(String[] args) {
        //创建两个线程
        Thread t1 = new MyThread1();
        Thread t2 = new MyThread2();
        //启动线程
        t1.start();
        t2.start();

    }
}

//第一个线程
class MyThread1 extends Thread{
    public void run(){
        for (int i=0;i<1000;i++){
            System.out.println("hello姐~");
        }
    }
}
//第二个线程
class MyThread2 extends Thread{
    public void run(){
        for (int i=0;i<1000;i++){
            System.out.println("来了~老弟!");
        }
    }
}

第一种创建线程的

优点:结构简单,利于匿名内部类形式创建。

缺点:
   1:由于java是单继承的,这会导致继承了Thread就无法再继承其他类去复用方法
   2:定义线程的同时重写了run方法,这等于将线程的任务定义在了这个线程中导致线程只能干这件事。重(chong)用性很低。
 

2.实现Runnable接口,实现run抽象类

public class ThreadDemo02 {
    public static void main(String[] args) {
        //创建线程任务
        MyRunnable01 myRunnable01 = new MyRunnable01();
        MyRunnable02 myRunnable02 = new MyRunnable02();
        //创建线程并指派任务
        Thread t1 = new Thread(myRunnable01);
        Thread t2 = new Thread(myRunnable02);
        //启动线程
        t1.start();
        t2.start();
    }
}

class MyRunnable01 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("大冤种##########");
        }
    }
}

class MyRunnable02 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("小牛马********");
        }
    }
}

3.匿名内部类创建线程

public class lamdaThreadDemo {
    public static void main(String[] args) {
        //Thread匿名内部类的写法
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("小牛马######");
                }
            }
        };

        //Runnable的匿名内部类写法
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                System.out.println("大冤种******");
            }
        });

        //启动线程
        thread1.start();
        thread2.start();
    }
}

三、多线程机制

1.多线程

当运行程序时,会启动一个进程,进程会启动一个main线程(主线程),当main进程启动一个子线程Thread-0(new一个进程对象),main线程不会阻塞,会继续执行,这时的main线程和Thread-0线程是交替执行的。
当主线程结束,子线程不会结束。当主线程和子线程都结束,才会结束进程。

在终端使用JConsole可以监视线程执行情况

2.start方法

为什么启动线程,调用的是start()方法,而不是run方法?
直接调用run方法,就相当于执行普通方法,没有开启线程,当run方法执行完,才会接着去执行main方法。
如果调用start方法就会开启一个线程,run方法和main方法会交替执行。
只有调用start方法才会真正的启动一个线程。

start方法源码解读
调用start方法,start方法会调用执行start0方法,由start0去调用run方法,JVM调用start0后,start0方法将run变成可执行状态,然后由CPU去统一去调度。
真正实现多线程的时start0方法。

四、多线程的API方法

1.API方法

start() //启动线程
currentThread(); //获取运行该方法的线程
getName(); //获取线程名
getId(); //获取线程id
getPriority() //获取线程优先级
isAlive() //判断该线程是否还活着
isDaemon //判断该线程是否是守护线程
isInterrupted() //判断该线程是否被中断

setDaemon() //设置守护线程
setName() //设置线程名字 
setPriority(Thread.MAX_PRIORITY); //设置线程优先级
sleep() //线程休眠
interrupt(); //唤醒线程休眠
wait() //将线程设置成等待状态
notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程,进入等待运行状态;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

yield():线程的礼让,让出CPU,使当前线程由执行状态,变成为就绪状态,在下一个线程执行时候,此线程有可能被执行,也有可能没有被执行。
join():线程的插队,让CPU优先执行该线程,一旦插队成功,会先执行完该线程的所有任务。

2.常用API方法实例:

public class ThreadInfoDemo {
    public static void main(String[] args) {
        //currentThread(); 获取主线程
        Thread mainThread = Thread.currentThread();

        //getName(); 获取线程名
        String name = mainThread.getName();
        System.out.println("线程名:"+name);

        //设置线程名字
        mainThread.setName("小牛马");
        System.out.println("线程名:"+ mainThread.getName());

        //getId(); 获取线程id
        long id = mainThread.getId();
        System.out.println("线程id:"+id);

        //getPriority() 获取线程优先级
        int priority = mainThread.getPriority();
        System.out.println("线程优先级:"+priority);

        //isAlive() 判断该线程是否还活着
        boolean life = mainThread.isAlive();
        System.out.println("线程是否还活着:"+life);

        //isDaemon 判断该线程是否是守护线程
        boolean guard = mainThread.isDaemon();
        System.out.println("线程是否是守护线程:"+guard);

        //isInterrupted() 判断该线程是否被中断
        boolean interrupt = mainThread.isInterrupted();
        System.out.println("线程是否被中断:"+interrupt);
    }
}

3.设置线程优先级

线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片.线程调度器尽可能均匀的将时间片分配给每个线程.
线程有10个优先级,使用整数1-10表示:
    - 1为最小优先级,10为最高优先级.5为默认值
    - 调整线程的优先级可以最大程度的干涉获取时间片的几率.优先级越高的线程获取时间片的次数越多,反之则越少.
public class PriorityDemo {
    public static void main(String[] args) {
        //第一个线程:
        Thread max = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println("熊大");
                }
            }
        };

        //第二个线程
        Thread min = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println("熊小小");
                }
            }
        };

        //第三个线程
        Thread norm = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println("小牛马");
                }
            }
        };

        //设置线程优先级:优先级1-10级,默认5级
        max.setPriority(Thread.MAX_PRIORITY);
        min.setPriority(Thread.MIN_PRIORITY);

        //启动线程
        max.start();
        norm.start();
        min.start();
    }
}

4.休眠线程

1)线程阻塞:sleep
线程提供了一个静态方法: static void sleep(long ms)
    - 使运行该方法的线程进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行.

2)sleep方法处理异常:InterruptedException.
当一个线程调用sleep方法处于睡眠阻塞的过程中,该线程的interrupt()方法被调用时,sleep方法会抛出该异常从而打断睡眠阻塞.
public class SleepDemo02 {
    public static void main(String[] args) {
        //蓝线程:
        Thread blue = new Thread(){
            @Override
            public void run(){
                System.out.println("线程休眠");
                try {
                    Thread.sleep(9999999);
                } catch (InterruptedException e) {
                    System.out.println("线程休眠唤醒");
                }
                System.out.println("线程休眠结束");
            }
        };

        //红线程:
        Thread red = new Thread(){
            public void run(){
                System.out.println("输出数据");
                for(int i=0;i<5;i++){
                    System.out.println("red: "+i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                }
                System.out.println("输出结束!");

                //中断lin的睡眠阻塞
                blue.interrupt();
            }
        };

        //启动线程
        blue.start();
        red.start();
    }
}

5.join():

线程的插队,让CPU优先执行该线程,一旦插队成功,会先执行完该线程的所有任务。

/**
 * 龟兔赛跑:
 * 先让兔子跑50步,然后乌龟在一直跑完,乌龟结束后,兔子再继续跑
 */
public class JoinDemo {
    public static void main(String[] args) {
        //乌龟线程
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    System.out.println("乌龟在跑!!!"+i);
                }
            }
        };
        int i = 1;
        while (i <= 100){
            System.out.println("兔子在跑!!!"+i);
            i++;
            if (i==50){
                t1.start();
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5.用户线程和守护线程

1)用户线程和守护线程
1.用户线程:也叫工作线程,当线程的任务执行完或通知方式结束。
2.守护线程:一般为工作线程服务的,当所有的用户线程结束,守护线程自动结束。


2)守护线程也称为:后台线程
    - 守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异.
    - 守护线程的结束时机上有一点与普通线程不同,即:进程的结束.
    - 进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程.
    - 常见的守护线程:垃圾回收机制
进程结束:
    当Java进程中所有的普通线程都结束时,进程就会结束,此时它会杀死所有还在运行的守护线程。

//将线程设置成守护线程,设置守护线程必须在线程启动前进行

线程.setDaemon(true);

3)设置守护线程

public class DaemonThreadDemo {
    public static void main(String[] args) {
        //第一线程
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("小牛马");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        //第二线程
        Thread t2 = new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("大冤种");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        t1.start();
        //必须在启动线程前设置守护线程
        t2.setDaemon(true);
        t2.start();
    }
}

五、线程的生命周期

一个线程从创建到消亡的生命周期大致分为五个状态:新建状态、可运行状态、运行状态、阻塞状态、消亡状态。

新建状态(new)
使用new创建一个新线程对象,该线程就处于新建状态
Thread myThread = newMyThread();

等待运行状态(就绪状态 Runnable)
当线程创建后,调用了start()方法便进入该状态,会产生所需的系统资源,并在就绪队列等待执行
myThread.start();

执行状态(Running)
当可运行状态的线程被CPU调用并获得系统资源,便进入执行状态,这时线程贵按顺序执行run()中每一条语句

阻塞状态(Blocked)
当线程发生了以下几种情况就进入阻塞状态
1.等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
2.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
3.其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

解除阻塞状态
1)如果线程处于休眠状态,当设定的休眠时间过后,便可进入运行状态,或者强制唤醒interrupt();
2)如果线程正在等待某个条件,需要调用该条件所在对象的notify()或notifyALL()方法,便可进入运行状态
3)如果线程因为I/O阻塞,当I/O操作结束后,阻塞线程便可回到运行状态

消亡状态(Dead)
当线程退出,不在执行,就是消亡状态。
线程的消亡状态分为自然消亡和强制消亡。
自然消亡是线程从run()正常退出。
强制消亡是线程被强制终止,如调用Thread的destroy()或stop()命令终止程序。

六、线程同步 synchronized

1.多线程并发安全问题:

当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪. 临界资源:操作该资源的全过程同时只能被单个线程完成. yield(): 出让CPU,进行线程切换,让当前运行线程回到等待运行状态。

2、synchronized关键字(同步锁,独占锁)

1)线程同步的概念:
线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。

2)synchronized有两种使用方式
    - 在方法上修饰,此时该方法变为一个同步方法
    - 同步块,可以更准确的锁定需要排队的代码片段

同步监视器对象的选取:
对于同步的成员方法而言,同步监视器对象不可指定,只能是this
对于同步的静态方法而言,同步监视器对象也不可指定,只能是类对象
对于同步块而言,需要自行指定同步监视器对象,选取原则:
    1.必须是引用类型
    2.多个需要同步执行该同步块的线程看到的该对象必须是同一个

第一种使用方式:线程同步方法

public class SyncDemo01 {
    public static void main(String[] args) {
        //让两个线程一起抢豆子
        Table table = new Table();
        //第一线程
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while(true){
                    //抢一个豆子
                    int bean = table.getBeans();
                    //出让CPU,让其他线程运行
//                    Thread.yield();
                    System.out.println(getName()+":"+bean);
                }
            }
        };
        //第二线程
        Thread t2 = new Thread(){
            @Override
            public void run() {
                while(true){
                    //抢一个豆子
                    int bean = table.getBeans();
                    //出让CPU,让其他线程运行
                    Thread.yield();
                    System.out.println(getName()+":"+bean);
                }
            }
        };
        //启动多线程
        t1.start();
        t2.start();
    }
}

class Table{
    private int beans = 20; //桌上有20个豆子
    //线程同步方法:获取豆子
    public synchronized int getBeans(){
        //当beans等于0,抛出异常,没有豆子了
        if (beans == 0){
            throw new RuntimeException("没有豆子啦!");
        }
        Thread.yield();
        return beans--;
    }
}

第二种使用方式:线程同步块

同步监视器对象即上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个。
public class SyncDemo02 {
    public static void main(String[] args) {
        Shop shop = new Shop();
        //第一线程
        Thread t1 = new Thread(){
            @Override
            public void run() {
                shop.buy();
            }
        };
        //第二线程
        Thread t2 = new Thread(){
            @Override
            public void run() {
                shop.buy();
            }
        };
        //启动线程
        t1.start();
        t2.start();
    }
}

class Shop{
    public void buy(){
        Thread t = Thread.currentThread();
        try {
            System.out.println(t.getName()+":正在挑衣服...");
            Thread.sleep(3000);
            //使用同步块,需要指定上锁对象
            synchronized (this){ //同一时间,只能有一个线程操作该代码块
                System.out.println(t.getName()+":正在试衣服...");
                Thread.sleep(3000);
            }
            System.out.println(t.getName()+":结账离开");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3)在静态方法上使用线程同步方法

    当在静态方法上使用synchronized后,该方法是一个同步方法.由于静态方法所属类,所以一定具有同步效果.
    静态方法使用的同步监视器对象为当前类的类对象(Class的实例).
    注:类对象会在后期反射知识点介绍.

public class SyncDemo03 {
    public static void main(String[] args) {
        //第一线程
        Thread t1 = new Thread(){
            @Override
            public void run() {
                Boo.dosome01();
            }
        };
        //第二线程
        Thread t2 = new Thread(){
            @Override
            public void run() {
                Boo.dosome01();
            }
        };
        //启动线程
        t1.start();
        t2.start();
    }
}

class Boo{
    public synchronized static void dosome01(){
        //获取该方法的线程
        Thread t = Thread.currentThread();
        try {
            System.out.println(t.getName()+"正在执行dosome方法...");
            Thread.sleep(5000);
            System.out.println(t.getName()+"执行dosome方法结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    }
}

4)在静态方法上使用线程同步块

静态方法中使用同步块时,指定的锁对象通常也是当前类的类对象
public class SyncDemo03 {
    public static void main(String[] args) {
        //第一线程
        Thread t1 = new Thread(){
            @Override
            public void run() {
                Boo.dosome02();
            }
        };
        //第二线程
        Thread t2 = new Thread(){
            @Override
            public void run() {
                Boo.dosome02();
            }
        };
        //启动线程
        t1.start();
        t2.start();
    }
}
class Boo{
    public static void dosome02(){
        //静态方法中使用同步块时,指定同步监视器对象通常还是用当前类的类对象
        //获取方式为:类名.class
        synchronized (Boo.class) {
            Thread t = Thread.currentThread();
            try {
                System.out.println(t.getName() + ":正在执行dosome方法...");
                Thread.sleep(5000);
                System.out.println(t.getName() + ":执行dosome方法完毕!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


​​​七、互斥锁

1.基本介绍:

Java语言中,引入了对象互斥锁的概念,来保证共亨数据操作的完整性。
    每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
    关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问

2.使用情况:

当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的.
使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的.

3.注意事项:

1.同步方法如果没有使用static修饰,默认锁对象为this。
2.如果方法使用static修饰,默认锁对象为:当前类.class。
3.一般同步代码块的范围比同步方法的范围小,所以效率高一些。
4.多线程的锁对象必须为同一个对象。

4.互斥锁实例

/**
 * 互斥锁
 * 当使用synchronized锁定多个不同的代码片段,并且指定的同步监视器对象相同时,
 * 这些代码片段之间就是互斥的,即:多个线程不能同时访问这些方法。
 */
public class SyncDemo04 {
    //主程序
    public static void main(String[] args) {
        Foo foo = new Foo();
        //第一个线程:执行A方法
        Thread t1 = new Thread(){
            @Override
            public void run() {
                foo.methodA();
            }
        };
        //第二个线程:执行A方法
        Thread t2 = new Thread(){
            @Override
            public void run() {
                foo.methodB();
            }
        };
        //启动线程
        t1.start();
        t2.start();
    }
}
//创建两个方法,并添加线程锁,指定相同的同步监视器对象
class Foo{
    //A方法
    public synchronized void methodA(){
      Thread t = Thread.currentThread();
        try {
            System.out.println(t.getName() + ":正在执行A方法...");
            Thread.sleep(5000);
            System.out.println(t.getName() + ":A方法执行完毕!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //B方法
    public synchronized void methodB(){
        Thread t = Thread.currentThread();
        try {
            System.out.println(t.getName() + ":正在执行B方法...");
            Thread.sleep(5000);
            System.out.println(t.getName() + ":B方法执行完毕!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


六、死锁

​​​1.线程死锁

所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
线程死锁产生的四个条件
(1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
(2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
(3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

2.释放锁

1.当前线程的同步方法或同步代码块执行结束
2.当前线程在同步方法或同步代码块中遇到break、return
3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
4.当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前程序暂停,并释放锁。

3.不会释放锁的情况

1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前程序的执行,不会释放锁。
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,不会释放锁。


##

我见青山如见君,君若见山亦如是。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值