Java碎碎念之多线程

一、基本概念

  • 程序(program) :是为了完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有他自身的产生、存在和消亡的过程。—生命周期
    >如:运行中的qq…
    >程序是静态的,进程是动态的
    >进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域.
  • 线程(thread) :进程可以进一步细分为线程,是一个程序内部的一条执行路径.
    >如果一个进程同一时间执行多个线程,就是支持多线程的
    >线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小
    >一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一堆中分配对象,可以访问相同的变量和对象.这就使得线程通信更简洁,高效,但是,多个线程共享同一资源可能会造成安全隐患.
  • 单核cpu和多核cpu的理解:
    >单核:其实是一种假的多线程,因为在一个时间单元内,只能执行一个线程的任务,但是因为cpu时间特别短,因此一般感觉不出来.
    >如果是多核,才能更好的发挥多线程的效率.
    >一个就Java程序至少包含三个线程:Java主线程,gc,异常处理线程.
  • 并行与并发:
    >并行:多个cpu同时执行多个任务,多个人做不同的事情.
    >并发:一个cpu(时间片)同时执行多个任务.比如:秒杀,多个人同事做意见事情.
  • 使用多线程的优点:背景:以单核cpu为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程用的时间短(因为cpu中切换线程需要耗时),那为何仍然需要多线程呢?
    多线程程序的优点:
    1)提高应用程序的响应,对于图形化界面更有意义,增强用户的体验.
    2)提高计算机cpu的利用率
    3)改善程序的结构,将即长有复杂的进程分为多个线程,独立运行,利于理解和修改.
  • 核实需要用到多线程
    1)程序需要执行两个或多个任务.
    2)程序需要实现一些需要等待的任务时,如:用户输入、文件读写操作、网络操作。
    3)需要一些后台运行的程序时。

二、线程的创建和使用

2.1多线程的创建
一、方式一:继承thread类
1)创建一个继承于thread类的子类
2)重写thread类中的run()–>将此线程执行的操作声明在run()中
3)创建thread类的子类对象
4)通过此子类对象调用start():启动当前线程,调用当前线程的run().
问题一:不能通过调用run()方法来启动线程.
问题二:一个已经拿启动的线程不能再次通过start方法来调用,会报异常:IllegalThreadStateException
thread买票:

package tickets;

/**
 * @author TR
 * @date 2020/7/29 - 下午 5:16
 * @describe:
 */
public class TicketsTest2 {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.start();w1.setName("窗口一");
        w2.start();w2.setName("窗口二");
        w3.start();w3.setName("窗口三");
    }
}

class Window extends Thread {
    private static int num = 100;

    @Override
    public void run() {
        if (num > 0) {
            for (int i = 0; i <= 100; i++) {
                num--;
                if (num < 0) {System.out.println("票额不足!");
                    break;
                }
                System.out.println(getName()+"售票成功!剩余票数为:" + num);
            }
        }
    }
}

   new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "   " + i);
                    }
                }
            }
        }.start();

如果只用一次的线程,可以考虑上面的创建方式.
二、’方式二:实现runnable接口
1)创建一个实现runnable接口的类
2)实现类去实现runnable接口中的抽象方法:run()
3)创建实现类的对象
4)将此对象作为参数传递到Thread类的构造器中,创建thread类的对象
5)通过thread类的对象调用start方法.
Runnable买票(三个线程共用一个Tickets对象,所以不用加static
)

package tickets;

/**
 * @author TR
 * @date 2020/7/29 - 下午 7:06
 * @describe:
 */
public class TicketNumbers3 {
    public static void main(String[] args) {
        Tickets t1 = new Tickets();
        Thread tt1 = new Thread(t1);
        Thread tt2 = new Thread(t1);
        Thread tt3 = new Thread(t1);
        tt1.setName("窗口一");
        tt2.setName("窗口二");
        tt3.setName("窗口三");
        tt1.start();
        tt2.start();
        tt3.start();
    }
}

class Tickets implements Runnable {
    public int num = 100;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "取出了1票,剩余:" + num);
                num--;
            }
        }
    }
}

三、两种方式的对比
开发中:优先选择实现runnable接口的方式
原因:1、实现的方式没有类的单继承的局限性
2、实现的方式更适合来处理多个线程有共 享的数据的情况。因为实现类只创建了一个
联系:Thread也是实现了Runnable接口的一个实现类。
相同点:两种方式都需要重写run(),将线程要执行的代码声明到run()中。

三、线程中常见的方法

1)start():启动当前线程:调用当前线程的run();
2)run():通常重写Thread中的方法,将创建的线程要执行的操作声明在此方法中.
3)currentThread():静态方法返回执行当前代码的线程.
4)getName():获取当前线程的名字;
5)setName ():设置当前线程的名字
6)yield():释放当前cpu的执行权.
7)join():在线程a中调用线程b的 join(),此时线程a就进入阻塞状态,直到线程b完全执行以后,线程a才结束阻塞状态.
8)stop():已过时,当执行此方法时,强制结束当前线程.
9)sleep(long milliontime):让当前线程"睡眠"指定的毫秒数,在指定的毫秒时间内,当前线程是阻塞状态.注意:此方法有异常,且只能try-catch,因为在重写之前的run方法中,并没有抛出异常,作为重写的子类,所抛出的异常只能小于等于父类中声明的异常.
10)isAlive():判断当前线程是否存活.

四、线程的优先级

1)MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5(默认优先级)
2)如何获取和设置当前线程的优先级:
>getPriority():获取当前线程的优先级
>setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级的线程cpu的执行权.但是只从效率上讲,高优先级的线程高概率下被执行.但是并不意味着只有当高优先级的线程执行完之后,低优先级的线程才执行.

五、线程的安全问题

方式Ⅰ 、同步代码块
语法:synchronized(同步监视器){需要被同步的代码}
说明:1、操作共享数据的代码,即为需要被同步的代码。(包含的代码要恰到好处)
2、共享数据:多个线程共同操作的变量。比如购票中的tickets。
3、同步监视器,俗称:锁。任何一个类的对象。都可以充当锁。要求:多个线程必须共用同一把锁。
补充:在实现runnable接口创建多个线程的时候,可以考虑使用this来充当同步监视器.thread不能用的原因是采用thread创建线程时,创建了多个对象,this不唯一.
方式Ⅱ、同步方法
使用synchronized同步的方式,优点:解决了线程安全的问题。
缺点:操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个线程的过程,效率低。

关于同步方法的总结:
1)同步一方法任然涉及到同步监视器,只是不需要我们显示的去声明
2)非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身.

六、线程死锁问题及其解决方案

1)死锁

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 出现了死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

2)解决方案

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
  • 使用lock锁
    (lock锁代码演示)
package threadsafe;

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

/**
 * @author TR
 * @date 2020/8/5 - 下午 4:05
 * @describe:
 */
public class LockTest {
    public static void main(String[] args) {
        Tickets t = new Tickets();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

class Tickets implements Runnable {
    static int num = 100;
    private ReentrantLock lock = new ReentrantLock(true);//创建锁对象

    @Override
    public void run() {
        while (true) {


            try {
                lock.lock();//加锁
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "  抢到了一票" + "票号为: " + num);
                    num--;
                } else {
                    System.out.println("票数不足!");
                    break;
                }
            } finally {
                lock.unlock();//释放锁

            }

        }
    }
}

synchronized与lock的对比
1)Lock是显示锁(手动开启和关闭锁,别忘记关闭锁),sychronized是隐式锁,出了作用域自动释放.
2)Lock只有代码块锁,synchronized有代码块锁和方法锁
3)使用lock锁居民将花费较少的时间来调度线程.并且具有更好的扩展性(提供更多的子类)
优先使用顺序:Lock–>同步代码块(已经进入方法体,分配相应的资源)–>同步方法(在方法体之外)

七、线程间的通信

涉及到的三个方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器.
  • notify():一旦执行此方法,就会唤醒被wait的一个线程.如果有多个线程被wait,就唤醒优先级较高的线程.
  • notifyAll():一旦被执行,就会唤醒所有被wait线程.

说明

  • 这三个方法必须使用在同步代码块或者同步方法中.
  • 三个方法的调用者必须是同步代码块或同步方法中的监视器,否则,会出现异常.
  • 这三个方法都是定义在Java.lang.Object下
  • 代码展示
package threadsafe;

/**
 * @author TR
 * @date 2020/8/6 - 下午 2:49
 * @describe: 两个人买票, 你买一下, 我买一下
 */
public class Communication {
    public static void main(String[] args) {
        Print p = new Print();
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        t1.setName("客户1");
        t2.setName("客户2");
        t1.start();
        t2.start();

    }
}

class Print implements Runnable {
    private int tickets = 100;
    Object obj=new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                obj.notify();
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "取到了一票!剩余为:" + tickets);
                    tickets--;
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("票额不足!");
                    break;
                }
            }
        }
    }
   
}

sleep和wait的异同

  • 相同点:一旦执行都可以使得当前线程进入阻塞状态.
  • 不同点

1)两个方法声明的位置不同,Thread类中声明sleep(),Object类中声明wait().
2)调用的方法不同:sleep()可以在任何需要的场景下调用.,wait()必须使用在同步代码块中.
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放锁,而wait()会释放.

八、线程创建的另外两种方式

1)实现callable接口—JDK5新增
步骤:

  • 创建一个实现runnable接口的实现类。
  • 实现call方法,将此线程需要执行的代码声明在call()中
  • 创建callable实现类的对象
  • 将此实现类的对象作为参数传到FutherTask的构造器中,创建FutherTask对象
  • 将FutherTask的对象作为参数传递到thread类的构造器中,并调用start().
  • 获取callable中call的返回值
  • get()返回值就是构造器参数实现call方法的返回值.

ps:如何理解实现callable接口比实现runnable接口更加强大?
a)call()可以有返回值
b)call()可以抛出异常,被外面的操作捕获,获取异常的信息.
c)call()支持泛型.
代码展示:

package callableTest;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author TR
 * @date 2020/8/8 - 下午 12:15
 * @describe:
 */
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        Sum s = new Sum();
        Sum1 s1 = new Sum1();
        Sum3 s2 = new Sum3();
        FutureTask f = new FutureTask(s);
        FutureTask f1 = new FutureTask(s1);
        FutureTask f2 = new FutureTask(s2);
        new Thread(f).start();
        new Thread(f1).start();
        new Thread(f2).start();
        Object o = f.get();//得到call方法的返回值
        Object o1 = f1.get();//得到call方法的返回值
        Object o2 = f2.get();//得到call方法的返回值
        System.out.println("1~100的和:" + o);
        System.out.println("1~100的偶数和:" + o1);
        System.out.println("1~100的奇数和:" + o2);
        long end = System.currentTimeMillis();
        System.out.println("多线程耗时" + (end - start));

    }
}

class Sum implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

class Sum1 implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                sum += i;
            }
        }
        return sum;
    }
}

class Sum3 implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0) {
                sum += i;
            }
        }
        return sum;
    }
}

2)线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可避免频繁创建、销毁。

好处:1)提高相应速度(减少了创建线程的时间).
2)降低资源的消耗(重复利用线程池中的线程,不必每次都创建)
3)便于线程管理
代码展示

package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author TR
 * @date 2020/8/8 - 上午 11:38
 * @describe:
 */
public class ThreadPool {
    public static void main(String[] args) {
        //提供指定数量的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) executorService;
        //设置线程池的属性
        int corePoolSize = service1.getCorePoolSize();//核心池的大小
        int maximumPoolSize = service1.getMaximumPoolSize();//最大线程数
        //执行指定的线程的操作.需要提供实现Runnable接口或者Callable接口的实现类
        executorService.execute(new Number());
        executorService.execute(new Number1());
        //关闭线程池
        executorService.shutdown();
    }
}

class Number implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + "  " + i);
            }

        }
    }
}

class Number1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + "  " + i);
            }

        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我只会javase

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值