线程

创建线程的三种方式

1、继承Thread类
2、实现Runnable
3、实现Callable


继承Thread

public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i =0; i<=10;i++){
            System.out.println("Thread线程执行"+i+"次:时间"+ LocalTime.now());
        }
    }
}

实现Runnable

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i =0; i<=10;i++){
            System.out.println("Runable线程执行"+i+"次:时间"+ LocalTime.now());
        }
    }
}

实现Callable

    public String call() throws Exception {
        for(int i =0 ;i<=10;i++){
            System.out.println("Callable主线程执行"+i+"次:时间"+ LocalTime.now());
        }
        return "完成";
    }

主线程main

public class ThreadCreateDemo {
    public static void main(String[] args) {
//        1、创建自定义线程类实例
//        2、启动线程
//        3、在main线程打印信息
//        一、Thread线程
//        MyThread myThread =new MyThread();
//        myThread.run();
//        二、Runnable线程
//        Thread thread=new Thread(new MyRunnable());
//        thread.start();
//        三、Callble线程
        FutureTask futureTask =new FutureTask<>(new MyCallable());
        Thread thread =new Thread(futureTask,"MyCallable");
        thread.start();
        try {

            System.out.println("Callable结果为:"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        for(int i =0 ;i<=10;i++){
            System.out.println("主线程执行"+i+"次:时间"+ LocalTime.now());
        }
    }
}

线程的生命周期

线程_第1张图片
image.png

新建
new关键字创建了一个线程之后,该线程处于新建状态
jvm为线程分配内存,初始化成员变量

就绪
当线程对象调用了start()方法之后,该线程处于就绪状态
jvm为线程创建方法栈和程序计数器,等待线程调度器调度

运行
就绪状态的线程获取cpu资源,开始运行run()方法,该线程处于运行状态

阻塞
当发生如下情况时,线程会进入阻塞状态

  • 线程调用sleep()方法主动放弃所占用的处理器资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程在等待某个通知(notify)
  • 线程视图获得一个同步锁(同步监视器),但该同步锁正在被其它线程所持有的
  • 程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免发方法

死亡
线程会在以下情况时,被处于死亡状态

  • run()或call()方法执行完成时,线程正常结束
  • 线程跑出一个未补货的Exception或Error
  • 调用该线程的stop()方法结束该线程,该方法容易导致死锁,不推荐使用

线程池Executor

创建和维护线程非常耗时,在企业开发中常见的办法是创建线程池,线程的创建和维护交给线程池处理。


线程_第2张图片
image.png
public class ExecutorsCreateDemo {
    //使用Executors获取线程对象
    //通过线程池对象获取线程,并执行myRunnable实例
    //分别打印线程池和主线程输出
    public static void main(String[] args) {
//        创建并设置线程池的大小
        ExecutorService executorService =Executors.newFixedThreadPool(5);
//        执行
        executorService.execute(new MyRunnable());
        for(int i =0 ;i<=10;i++){
            System.out.println("主线程执行"+i+"次:时间"+ LocalTime.now());
        }
    }
}

线程安全

如果多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行结果是一样的,二其他的变量的值也和预期一致,就是线程安全;反之,线程不安全。


单线程卖火车票

火车票对象

public class Ticket implements Runnable {
    //    假设票有100张
    private int ticketNum = 100;

    @Override
    public void run() {
        while (true) {
            if (ticketNum > 0) {
//                有票,让线程睡眠100毫秒
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
//                打印当前售出的票数和线程名
                String threadName = Thread.currentThread().getName();
                System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum-- + "张");
            }
        }
    }
}

卖票窗口(一个线程一个窗口)

public class ThreadSafeDemo {
    public static void main(String[] args) {
//        创建电影票对象
        Ticket ticket = new Ticket();
//        创建一个线程:售票窗口
        Thread thread = new Thread(ticket, "售票口1");
        thread.start();
    }
}

一个线程时,观察结果没有任何问题!
对上面代码进行修改,增加一个买票窗口,再观察结果!

加入了一个买票窗口

public class ThreadSafeDemo {
    public static void main(String[] args) {
//        创建电影票对象
        Ticket ticket = new Ticket();
//        创建一个线程:售票窗口
        Thread thread1 = new Thread(ticket, "售票口1");
        thread1.start();
        Thread thread2 = new Thread(ticket, "售票口2");
        thread2.start();
        Thread thread3 = new Thread(ticket, "售票口3");
        thread3.start();
    }
}
线程_第3张图片
image.png
  • 此时出现了线程不安全的情况

当线程thread(窗口1)抢占到了cpu的执行权后并调用了sleep()方法后,线程会处于阻塞状态,ticketNum没有进行自剪操作,当窗口2拿到的票数还是之前的,就造成了两个窗口打印的票数一样的结果。

问题分析

线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读不写,一般来说,这个变量线程安全。
若多个线程同时执行写的操作,一般都需要考虑线程同步,否则可能会影响线程安全。

解决方法-线程同步

要解决以上问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺cpu资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的对象。
java引用了7中线程同步机制。

  • 同步代码块(synchronized)
  • 同步方法(synchronized)
  • 同步锁(ReenreantLock)
  • 特殊域变量(volatile)
  • 局部变量(ThreadLocal)
  • 阻塞队列(LinkedBlockingQueue)
  • 原子变量(Atomic*)

加锁synchronized:在同步代码块或同步方法上加锁

public class Ticket implements Runnable {
    //    假设票有100张
    private int ticketNum = 100;

    private Object lock = new Object();

    @Override
    public void run() {
        while (true) {
//            synchronized (lock) {
//                if (ticketNum > 0) {
////                有票,让线程睡眠100毫秒
//                    try {
//                        Thread.sleep(100);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
////                打印当前售出的票数和线程名
//                    String threadName = Thread.currentThread().getName();
//                    System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum + "张");
//                    ticketNum--;
//                }
//            }
            saleTickt();
        }
    }

    synchronized void saleTickt() {
//        当该方法是非静态方法,synchronized锁的是调用该方法的对象
//        当该方法是static,synchronized锁的是当前类的字节码对象。synchronized(Ticket.class)
        if (ticketNum > 0) {
//                有票,让线程睡眠100毫秒
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
//                打印当前售出的票数和线程名
            String threadName = Thread.currentThread().getName();
            System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum + "张");
            ticketNum--;
        }
    }
}

结果:全是窗口1在卖票
上面方法看不到加锁的过程,所以有了同步锁(有好多实现类)
同步锁

public class Ticket implements Runnable {
    //    假设票有100张
    private int ticketNum = 100;

    //    重入锁
    // true:公平锁(每个线程都拥有拿到这个线程的权利);false:独占锁(只有某个线程执行完毕后才释放该锁)。独占锁相当于synchronized
    private Lock locked = new ReentrantLock(true);

    @Override
    public void run() {
        while (true) {
//            加锁
            locked.lock();
            try {
                if (ticketNum > 0) {
//                有票,让线程睡眠100毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
//                打印当前售出的票数和线程名
                    String threadName = Thread.currentThread().getName();
                    System.out.println("线程" + threadName + "当前剩余火车票" + ticketNum + "张");
                    ticketNum--;
                }
            } catch (Exception e) {

            } finally {
//                解锁(必须要解锁,否则会出现死锁)
                locked.unlock();
            }
        }
    }
}
线程_第4张图片
image.png

synchronized和Lock区别

synchronized是jvm内置关键字,在jvm层面,Lock是java的一个类。
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
synchronized会自动释放锁(线程一执行完同步代码会释放锁,线程二在执行过程中发生异常会释放锁),Lock需在finally中手动释放锁(调用unlock()方法),否则容易发生死锁
用synchronized关键字的两个线程1和2,如果当线程1获得锁,线程2等待。如果线程1阻塞,线程2则会一致等待下去;而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
synchronized的锁可重入,不可中断、非公平,而LOCK锁可重入,可判断、公平和独占都可设置
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码量少的同步问题


死锁的产生条件

互斥条件
进程要求对所有分配的资源(如打印机)进行排他性空值,即在一段时间内某资源仅为一个进程所战友。此时若有其他进程请求该资源,则请求进程只能等待

不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己释放(只能是主动释放)

请求与保持条件
进程已经保持了至少一个资源,但又提出新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已经获得的资源保持不放

循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求,即存在一个处于等待状态得集合(循环)。

你可能感兴趣的:(线程)