Java多线程基础


一. 线程 & 进程

进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间

线程: 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少 有一个线程 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分 成若干个线程


二. 线程调度

分时调度: 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度: 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),

Java使用的是抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时 刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使 用率更高。


三. 同步 & 异步, 并发 & 并行

同步: 排队执行 , 效率低但是安全.
异步: 同时执行 , 效率高但是数据不安全.

举个例子: 有一桌子菜, 大家排队吃饭就是同步, 一起同时吃饭就是异步。
排队吃饭效率较低, 但是你看到你喜欢的菜就没人跟你抢,很安全.
相反,一桌人同时吃饭效率较高, 但是当你看到一块肉并伸出筷子时, 可能已经被别人夹走了, 数据不安全。

并发: 两个或多个事件在同一个时间段内发生。
并行: 两个或多个事件在同一时刻发生(同时发生)。


四. 如何实现多线程?

1. 继承Thread类

第一步: 定义一个类继承Thread类
第二步: 重写run方法

public class MyThread extends Thread {
    //run方法就是线程要执行的任务方法
    @Override
    public void run() {
        //这里的代码就是一条新的执行路径
        //这个执行路径是触发方式,不是调用run方法,而是通过thread对象的start方法来启动任务
        for (int i = 0; i < 10; i++) {
            System.out.println("MyThread"+i);
        }
    }
}

第三步: 创建MyThread类的对象
第四步: 调用start方法, 启动线程

public class Demo1 {
    public static void main(String[] args) {

        MyThread m = new MyThread();
        m.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("Main"+i);
        } 
    }
}


2. 实现Runnable接口

第一步: 定义一个类实现Runnable类
第二步: 重写run方法

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程的任务
        for (int i = 0; i < 10; i++) {
            System.out.println("MyRunnable"+i);
        }
    }
}

第三步: 创建一个任务对象
第四步: 创建一个线程并给他一个任务
第五步: 调用start方法, 启动线程

public class Demo1 {
    public static void main(String[] args) {
        //1 创建一个任务对象
        MyRunnable r = new MyRunnable();
        //创建一个线程并给他一个任务
        Thread t = new Thread(r);
        //启动线程
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("Main"+i);
        }
    }
}


3. 为什么更推荐实现Runnable而不是继承Thread

实现Runnable与继承Thread相比有如下优势:

1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况

2.可以避免单继承所带来的局限性

3.任务与线程是分离的,提高了程序的健壮性

4.后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程


4. 匿名内部类

继承Thread类

public class MyThread0 {
    public static void main(String[] args) {
        new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Thread" + i);
                }
            }
        }.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main" + i);
        }
    }
}

或者实现Runnable接口

public class MyThread0 {
    public static void main(String[] args) {
		new Thread(new Runnable() {				
    	public void run() {			   
        for(int i = 0; i < 5; i++) {	
            System.out.println("Runnable" + i);
        	}
    	}
	}).start(); 
		for (int i = 0; i < 5; i++) {
    	System.out.println("Main" + i);
		}
	}
}

温馨提示: main方法其实也是一个线程, 也称为主线程。在java中所以的线程都是同时启动的,至于哪个线程先执行,这得看哪个线程先抢到CPU的资源。


五.sleep方法

修饰和类型方法描述
static voidsleep(long millis)使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
static voidsleep(long millis, int nanos)导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        //线程的休眠
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
            Thread.sleep(1000);     //1000毫秒
        }
    }
}
}

输出结果是从0-9的9个整数, 只不过没打印一个数都会停顿一秒


六.wait & notify方法

修饰和类型方法描述
voidwait()导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
voidwait(long timeout)导致当前线程等待,直到另一个线程调用 notify()方法或该对象的 notifyAll()方法,或者指定的时间已过。
voidwait(long timeout, int nanos)导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法,或者某些其他线程中断当前线程,或一定量的实时时间。
voidnotify()唤醒正在等待对象监视器的单个线程。
voidnotifyAll()唤醒正在等待对象监视器的所有线程。

这里我们通过一个生产者与消费者的例子来快速熟悉这两个方法
假设现在有三个类, 分别是厨师类, 服务员类, 和食物类.

大致的服务流程:

厨师每做好一道菜都会唤醒服务员并进入无限期等待状态, 当服务员把菜送上餐桌后便会将厨师唤醒让其开始做下一道菜, 自己则再一次进入等待状态, 循环100次.

我们先演示不加wait和notify的结果

厨师类

static class Cook extends Thread{
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if(i%2==0){
                    f.setNameAndSaste("土耳其冰淇凌","巧克力味");
                }else{
                    f.setNameAndSaste("麻婆豆腐","香辣味");
                }
            }
        }
    }

服务员类

    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }

食物类

 static class Food{
        private String name;
        private String taste;

 

        public void setNameAndSaste(String name,String taste){
           
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
            }
        
        public void get(){
        	System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);            
            }
        }
    }

Main方法

public class Demo  {
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }

运行结果:
在这里插入图片描述
问题很明显, 巧克力味的麻婆豆腐和香辣味的冰淇凌? 这就离谱!!!

解决方案: 在Food类中加一个标记flag

当flag为true时, 厨师可以开始做菜, 在做完菜后, 设置flag为false, 并唤醒服务员(让他上菜), 再让厨师线程进入等待状态

当flag为false时,服务员可以上菜,上菜后再设置flag变为true(厨师在flag为true时才能做菜), 再把厨师线程线程唤醒, 最后让自己(服务员)进入等待状态

改进的Food类

static class Food{
        private String name;
        private String taste;

        //true 表示可以生产
        private boolean flag = true;

        public synchronized void setNameAndSaste(String name,String taste){
            if(flag) {
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get(){
            if(!flag) {
                System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


七. 线程生命周期

在这里插入图片描述

  1. 一旦线程对象被new出来后,线程就进入了新建状态;

  2. 当该对象调用start()方法,就进入就绪状态;

  3. 进入就绪状态后,当该对象抢到CPU时间片就会进入运行状态;

  4. 进入运行状态后

  5. 4.1. 若run()方法或main()方法结束后,线程就进入终止状态;

    4.2. 当线程调用了自身的sleep()方法或其他线程的join()方法, 线程就会进入阻塞状态(注意: 调用sleep ()函数后,线程不会释放"锁")。当sleep()结束或join()结束后,该线程回到可运行状态,继续争抢CPU时间片。

    4.3. 当线程调用了yield()方法,该线程放弃当前获得的CPU时间片,回到就绪状态,这时与其他进程处于同等竞争状态。

    4.5. 当线程调用了suspend() 和 resume() 方法,suspend()使得线程进入阻塞状态,并且不会自动恢复,只有resume() 方法被调用,才能使得线程重新进入可运行状态。一般suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。

    4.6、wait() 和 notify() 方法:当线程调用wait()方法后会进入等待状态,进入这个状态后,只能依靠其他线程调用notify()或notifyAll()方法才能被唤醒(一般都用notifyAll()方法,唤醒有所线程)当调用wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用

注意:wait() 和 notify() 与suspend() 和 resume() 这两对方法看起来很相似,但是它们是有区别的。suspend()方法在线程阻塞时都不会释放占用的"锁"(如果占用了的话),而wait()方法的调用不会让线程释放掉占有的"锁"。


八. 线程安全

多线程为什么会出现安全问题呢?我们不妨先举个例子
三个售票窗口同时卖10张火车票(这里我们创建3个线程来模拟该情景)

public class Demo7 {
    public static void main(String[] args) {
        
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {
            while (count>0){
                //卖票
                System.out.println("正在准备卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println("卖票结束,余票:"+count);
            }
        }
    }
}

运行结果如下
在这里插入图片描述
结果发现出现了余票"增加"以及余票负数等问题
这是因为多线程在进行同一卖票任务时,当余票只剩一张时, 三个线程都已经进入到while(count>0)的语句里, 所以最后会出现负数的情况。

以下是三种解决方式

1. 同步代码块(隐式锁)

把synchronized关键字加上一个锁对象.
语法: synchronized(锁对象){}

public class Demo8 {
    public static void main(String[] args) {
        Object o = new Object();
        
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        private Object o = new Object();
        @Override
        public void run() {
            //Object o = new Object();    //这里不是同一把锁,所以锁不住
                while (true) {
                    synchronized (o) {
                        if (count > 0) {
                         //卖票
                            System.out.println("正在准备卖票");
                            try {
                            Thread.sleep(1000);
                            } catch (InterruptedException e) {
                            e.printStackTrace();
                            }
                            count--;
                            System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                        }else {
                            break;
                        }

                }
            }
        }
    }
}

2. 同步方法(隐式锁)

把synchronized关键字修饰在方法Sale()中
语法: public synchronized boolean sale(){}

public class Demo9 {
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案2  同步方法
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {

            while (true) {
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }
        }
        public synchronized boolean sale(){
            if (count > 0) {
                //卖票
                System.out.println("正在准备卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                return true;
            }
                return false;

        }
    }
}

3. Lock(显示锁)

语法:
1. 创建一把锁: Lock l = new ReentrantLock()
2. 加锁: l.lock()
3. 解锁: l.unlock()

public class Demo10 {
    public static void main(String[] args) {
        Object o = new Object();
        //线程不安全
        //解决方案1   显示锁  Lock  子类 ReentrantLock

        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        
        private Lock l = new ReentrantLock();
        @Override
        public void run() {
            while (true) {
                l.lock();
                    if (count > 0) {
                        //卖票
                        System.out.println("正在准备卖票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                    }else {
                        break;
                    }
                    l.unlock();
            }
        }
    }
}


九. 线程池

线程池, 顾名思义就是一个装线程的容器. 通过重用已存在的线程,降低线程创建和销毁造成的消耗. 线程若是无限制的创建,可能会导致内存占用过多,并且会造成cpu过度切换.

1. 缓存线程池

特点: 没有长度限制

流程:
1. 判断线程池是否存在空闲线程
2. 存在则使用
3. 不存在则创建线程并使用

在这里插入图片描述

public class Demo13 {
    public static void main(String[] args) {
        
        ExecutorService service = Executors.newCachedThreadPool();
        
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }

        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }

        });service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }

        });

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

2. 定长线程池

特点: 长度固定

流程:

1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程  且线程池未满的情况下  则创建线程  并放入线程池中  然后使用
4 不存在空闲线程  且线程池已满的情况下  则等待线程池的空闲线程
public class Demo14 {
   
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
    }
}

结果如下
在这里插入图片描述
由于线程池指定长度为2, 在执行第三个任务时并没有创建第3个线程, 而是由空闲下来的线程来执行

3. 单线程线程池

特点: 线程池里只有一个线程

流程:

    1 判断线程池的那个线程是否空闲
    2 空闲则使用
    3 不空闲则等待它空闲后再使用
public class Demo15 {
    /*单线程线程池
    执行流程
        1 判断线程池的那个线程是否空闲
        2 空闲则使用
        3 不空闲则等待它空闲后再使用
    **/
    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
    }
}

运行结果如下
在这里插入图片描述
三个任务全部由一个线程来执行

4. 周期定长线程池

流程:

    1 判断线程池是否存在空闲线程
    2 存在则使用
    3 不存在空闲线程  且线程池未满的情况下  则创建线程  并放入线程池中  然后使用
    4 不存在空闲线程  且线程池已满的情况下  则等待线程池的空闲线程

定时执行一次

public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //定时执行一次
        //参数1:定时执行的任务
        //参数2:时长数字
        //参数3:参数2的时间单位    Timeunit的常量指定
       scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        },5, TimeUnit.SECONDS);      //5秒钟后执行*/

周期性执行任务

public class Demo16 {
 
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        /*
        周期性执行任务
            参数1:任务
            参数2:延迟时长数字(第一次在执行上面时间以后)
            参数3:周期时长数字(没隔多久执行一次)
            参数4:时长数字的单位
        * **/
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        },5,1,TimeUnit.SECONDS);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值