java多线程基础

基本概念

  • 程序:为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有他自身的产生、存在、消亡的过程。即生命周期
    程序是静态的,进程是动态的
  • 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径,若一个进程同一时间并行执行多个线程,就是支持多线程的,进程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器。
  • 并行和并发:并行是多个cpu同时执行多个任务。比如:多个人同时做不同的事。并发:一个cpu(采用时间片)同时执行多个任务。比如:秒杀
  • 多线程优点
    1、提高应用程序的响应。对图形化界面更有意义,可增强用户体验,
    2、提高计算机系统cpu的利用率。
    3、改善程序结构。将既长又复杂的进程分为多个线程独立运行,利于理解和修改。
  • 何时需要多线程
    1、程序需要同时执行两个或多个任务。
    2、程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
    3、需要一些后台运行的程序时

线程创建和使用

方式一:继承Thread类

1、创建一个继承于Thread类的子类。
2、重写Thread类的run方法
3、创建Thread类的子类对象
4、通过此对象调用start()

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread1 = new Thread1();
        thread1.start();
    };
}
// 打印偶数
class Thread1 extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            if(i%2==0){
                System.out.println(Thread.currentThread()+":"+i);
            }
        }
    }
}
  • Thread中的常见方法:
    1、start():(1)启动当前线程;(2)调用当前线程的run()方法
    2、run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。
    3、currentThread():静态方法,返回执行当前代码的线程
    4、getName():获取当前线程的名字
    5、setName():设置当前线程的名字
    6、yield():释放当前cpu的执行权
    7、join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
    8、sleep(long milltime):让当前线程“睡眠”指定的milltime毫秒。在指定milltime毫秒时间内,当前线程是阻塞状态。
    9、isAlive():判断线程是否还存活

  • 线程的优先级
    线程的优先级等级:
    MAX_PRIORITY=10
    MIN_PRIORITY=1
    NORM_PRIORITY=5 (默认的优先级)
    获取线程的优先级:getPriority();
    设置线程的优先级:setPriority(int p);
    优先级高只代表先执行的概率高

  • 例子:使用继承Thread类的方式实现多窗口卖票(存在线程安全问题)

**
 * 例子:创建三个窗口卖票,总票数为100*/
class Windows extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {
        while (true){
            if(ticket>0){
                System.out.println(getName()+":卖票,票号为:"+ticket);
                ticket--;
            }
        }
    }
}

public class WindowsTest {
    public static void main(String[] args) {
        Windows w1 = new Windows();
        w1.setName("线程一");
        Windows w2 = new Windows();
        w2.setName("线程二");
        Windows w3 = new Windows();
        w3.setName("线程三");
        w1.start();
        w2.start();
        w3.start();
    }
}

此代码显然存在线程安全问题!!!在解决这个问题之前,先看一下创建多线程的第二种方式

方式二:实现Runnable接口

1、创建一个实现了Runnable接口的类
2、 实现类去实现Runnable中的抽象方法:run()
3、创建实现类的对象
4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5、通过Thread类的对象调用start()

public class ThreadTest1 {
    public static void main(String[] args) {
        // 3、创建实现类对象
        MyThread myThread = new MyThread();
        // 4、将实现类作为参数构建Thread对象
        Thread t1 = new Thread(myThread);
        // 5、启动线程
        t1.start();
    }
}
// 1、创建一个类实现Runnable接口
class MyThread implements Runnable{
    // 2、实现其中的run方法
    public void run() {
        for (int i=0;i<100;i++){
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}
  • 例子:使用实现Runnable接口的方式实现多窗口卖票(存在线程安全问题)
public class WindowsTest1 {
    public static void main(String[] args) {
        Window1 window1 = new Window1();
        Thread t1 = new Thread(window1);
        t1.setName("窗口一");
        Thread t2 = new Thread(window1);
        t2.setName("窗口二");
        Thread t3 = new Thread(window1);
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window1 implements Runnable{
    private int ticket =100;
    public void run() {
        while (true){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
                ticket--;
            }else {
                break;
            }
        }
    }
}

比较这两种创建线程的方式
1、实现Runnable的方式没有类的单继承的局限性
2、实现Runnable的方式更适合来处理多个线程有共享数据的情况
3、Thread也是实现了Runnable接口。class Thread implements Runnable
相同点:都需要重写run方法

线程分类:
java中线程分为:守护线程、用户线程
守护线程是用来服务用户线程的,通过start()方法前调用thread.setDaemon(true)可以把一个用户线程设置为守护线程

线程的生命周期

jdk中的Thread.state中定义了线程的几种状态:

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
  • 就绪:调用start()方法后;sleep()时间到;join()结束;获取同步锁;notify();notifyAll();
    resume()(已过时);
  • 运行:获取cpu的执行权
  • 阻塞:调用sleep();join();等待同步锁;wait();suspend()(已过时)
  • 死亡:执行完run()方法后;执行stop()(已过时)方法后;出现Error/Exception且没处理
    在这里插入图片描述

线程的同步

  • 1、问题:卖票的例子中,在卖票的过程中出现了错票和重票的问题。
  • 2、问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也来操作车票,就会出现安全问题。
  • 3、如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进去,直到线程a操作完ticket时,其他线程才可以操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
  • 4、在Java中,我们通过同步机制,来解决线程的安全问题。

方式一:同步代码块

synchronized(同步监视器){
//需要被同步的代码
}

说明:
1、什么是需要被同步的代码呢?即操作共享数据的代码
2、共享数据:多个线程共同操作的变量。比如这里的ticket
3、什么是同步监视器?同步监视器,俗称“”锁“”。任何一个类的对象,都可以充当锁。
4、要求:多个线程必须要共用一把锁!!!(补充:在实现Runnable接口的方式中,可以使用this充当同步监视器。在集成Thread类的实现方式中,慎用this充当同步监视器。)
5、同步的好处:解决了线程的安全问题;同步的缺点:在执行同步代码块的时候,是单线程的。

使用同步代码快的方式解决多窗口卖票的线程安全问题

public class WindowsTest1 {
    public static void main(String[] args) {
        Window1 window1 = new Window1();
        Thread t1 = new Thread(window1);
        t1.setName("窗口一");
        Thread t2 = new Thread(window1);
        t2.setName("窗口二");
        Thread t3 = new Thread(window1);
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window1 implements Runnable{
    private int ticket =100;
//    Object o = new Object();
    public void run() {
        while (true){
            synchronized(this) {//此时的this:就是当前对象,就是Window1的对象 //synchronized(o) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

方法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

使用同步方法解决实现Runnable接口存在的线程安全问题

**
 * 使用同步方法解决实现Runnable接口存在的线程安全问题
 */
public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();
        Thread t1 = new Thread(w);
        t1.setName("窗口一");
        Thread t2 = new Thread(w);
        t2.setName("窗口二");
        Thread t3 = new Thread(w);
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window3 implements Runnable{
    private int ticket =100;
    public void run() {
        while (true){
            synchronized(this) {
                show();
            }
        }
    }

    private synchronized void show(){//同步监视器:this
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

使用同步方法解决继承Thread方法的线程安全问题

/**
 * 使用同步方法解决继承Thread方法的线程安全问题
 */
public class WindowTest4 {
    public static void main(String[] args) {
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Window4 w3 = new Window4();
        w1.start();w2.start();w3.start();
    }
}
class Window4 extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {
        while (true){
            show();
        }
    }

    private static synchronized void show(){ // 同步监视器为:Window4.class
        if(ticket>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

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

单例模式:
使用同步机制将单例模式中的懒汉模式改写成线程安全的

/**
 * 使用同步机制将单例模式中的懒汉模式改写成线程安全的
 */
public class BankTest {
}
class Bank{
    private Bank(){}

    private static Bank instance = null;

//    public static synchronized Bank getInstance(){  // 同步方法解决线程安全问题,同步监视器默认为Bank.class
//        if(instance == null){
//            instance = new Bank();
//        }
//        return instance;
//    }

    public static Bank getInstance(){
        // 方式一:效率稍差
//        synchronized(Bank.class){   // 同步代码块的方式解决线程安全问题
//            if(instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }
        // 方式二:效率更高
        if(instance == null){
            synchronized(Bank.class){
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

线程的死锁问题
死锁:不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决办法:1、专门的算法、原则 2、尽量减少同步资源的定义3、尽量避免嵌套同步

方式三:Lock锁 —JDK5.0新增

1、实例化ReentrantLock
2、lock()
3、unlock()

public class ThreadTest {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window implements Runnable{
    private int ticket = 100;
    private ReentrantLock reentrantLock = new ReentrantLock(); // 1、实例化ReentrantLock
    public void run() {
        while(true){
            try{
                reentrantLock.lock(); // 2、上锁
                if(ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {

                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                }else {
                    break;
                }
            }finally {
                reentrantLock.unlock(); // 3、解锁
            }
        }
    }
}

面试题:synchronized和Lock的异同
相同:都是解决线程的安全问题
不同:synchronized机制执行完相应的同步代码快后,自动释放同步监视器。
Lock需要手动的启动同步(Lock()),同时结束同步也需要手动的实现(unLock())
面试题:如何解决线程安全问题?几种方式?
三种方式:1、同步代码块;2、同步方法;3、Lock锁

练习1: 银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】:
1、明确哪些代码是多线程运行代码,须写入run()方法
2、明确什么是共享数据
3、明确多线程运行代码中哪些语句是操作共享数据的。

public class ThreadTest1 {
    public static void main(String[] args) {
        Account account = new Account();
        Thread personA = new Thread(account);
        Thread personB = new Thread(account);
        personA.start();
        personB.start();
    }
}

/**
 * 账号类
 */
class Account implements Runnable{
    private double balance;  // 账户余额

    /**
     * 存钱
     * @param money 存的钱数
     */
    private synchronized void saveMoney(double money){
        if(money>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.balance = this.balance + money;
            System.out.println(Thread.currentThread().getName()+"存钱成功,账户余额为:"+this.balance);
        }
    }
    public void run() {
        // 分三次存入
        for(int i = 0;i<3;i++){
            saveMoney(1000);
        }
    }
}

线程的通信

例题:使用两个线程交替打印1-100

class Number implements Runnable{
    private int number = 1;
    public void run() {
        while (true){
            synchronized(this){
                notify(); // 唤醒阻塞的线程
                if(number <= 100){
                    try {
                        Thread.sleep(100);// sleep()不会释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    try {
                        // 使得调用如下wait()方法的线程进入阻塞状态;wait()会释放锁
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    break;
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.start();
        t2.start();
    }
}

三个方法:
wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器
notify():一旦执行此方法,就会唤醒被wait()的一个线程。如果有多个线程被wait,就唤醒优先级高德
notifyAll():唤醒所以被wait()的线程;
说明:
1、wait()、notify()、notifyAll()这三个方法必须使用在同步代码块或同步方法中。
2、wait()、notify()、notifyAll()这三个方法的调用者必须是同步代码块或同步方法中的同步监视器调用,否则会出现IllegalMonitorStateException异常
3、wait()、notify()、notifyAll()这三个方法是定义在java.lang.Object类中;

面试题:sleep()和wait()的异同?
1、相同点:一旦执行方法,都可以是的当前线程进入阻塞状态。
2、不同点:
1):两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
2):调用的要求不同。sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中
3):关于释放同步监视器:如果两个方法都是用在同步代码块或者同步方法中,sleep()不会释放锁,wait()会释放锁

经典例题:生产者/消费者问题

生产者(productor)将产品交给店员(clerk),而消费者(customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者取走产品。

  • 可能存在的两个问题:1、生产者比消费者快,消费者会漏掉一些数据没有取到。2、消费者比生产者快时,消费者会取相同的数据。

jdk5.0新增线程创建方式

方式三:实现Callable接口

public class CallableTest {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();
        FutureTask futureTask = new FutureTask(numThread);
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            //get()方法返回值即是FutureTask构造器参数Callable实现类的call方法的返回值
            Object o = futureTask.get();
            System.out.println(o);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class NumThread implements Callable{

    public Object call() throws Exception {
        int sum = 0;
        for(int i =1;i<=100;i++){
            if(i%2==0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

实现Callable接口比实现Runable接口的优势
1、call()可以抛出异常,有返回值
2、Callable支持泛型

方式四:使用线程池

好处:
1、提高响应速度(减少创建新线程的时间)
2、降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
3、便于线程管理

  public static void main(String[] args) {
        // 1、提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        // 设置线程池的属性
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        service1.setCorePoolSize(2);
        
        // 2、执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumThread1());  // 适合用于Runnable
        // service.submit(new NumThread());    // 适合用于Callable
        // 3、关闭连接池
        service.shutdown();
    }

几个问题:
1、画图说明线程的生命周期,以及各状态切换使用到的方法等。
2、同步代码块中涉及到同步监视器和共享数据,谈谈你对同步监视器和共享数据的理解,以及注意点
3、sleep()和wait()的区别
4、写一个线程安全的懒汉式
5、创建多线程有哪几种方式?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值