线程

线程的概念:

一个线程是指程序中完成一个任务的执行流,java中可以在一个程序中并发地运行多个线程,这些线程可以同时在多个处理器上运行
多个线程在多个CPU上运行
在单CPU系统中,多个线程分享CPU的时间,操作系统负责CPU资源的调度和分配
多个线程在单个CPU上运行



多线程可以使程序的反应更快,交互性更强,执行效率更高。当程序作为一个应用程序(application)运行时,jvm会为main方法创建一个线程,当程序不再需要时,jvm就会创建一个线程来进行垃圾回收的工作。所以一个完整的应用程序最少含有两个线程。

2. 创建线程

2.1 继承父类Thread

  1. 将类声明为Thread的子类
  2. 重写Thread类的run()方法
  3. 创建Thread的子类对象,并且调用start()方法,启动线程

public class MyThread extends Thread {

    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        for(int i=0;i<100;i++){
            System.out.println("main-->"+i);
        }
    }
    public void run() {

        for(int i=0;i<100;i++){
            System.out.println("自定义-->"+i);
        }
    }
}

2.2 实现Runnable接口

  1. 创建类并实现Runnable接口
  2. 实现Runnable接口里的run方法,完成自定义线程的任务代码
  3. 实例化类
  4. 创建Thread对象,参考Thread构造方法:Thread(Runnable target)
  5. 调用start()方法,启动线程
public class MyThread3 implements Runnable {

    public void run() {

        for(int i=1;i<100;i++){
            System.out.println(Thread.currentThread()+":"+i);
        }
    }

    public static void main(String[] args) {

        //创建类实例
        MyThread3 thread3 = new MyThread3();
        //创建Thread类的实例,把Runnable作为实参传递
        Thread thread = new Thread(thread3,"线程-C");
        //调用thread的start()方法启动线程
        thread.start();

        for(int i=1;i<100;i++){
            System.out.println(Thread.currentThread()+":"+i);
        }
    }
}

Note:因为java是单继承、多实现的,所以建议使用实现Runnable接口的方法来实现线程

3. 线程常见的方法

  1. 设置线程的名字
    可以使用带参数的构造方法Thread(String name) 或者thread.setName(“myThread”)方法,为线程命名。同样getName()可以得到线程的名字。
  2. 睡眠线程
    注意:sleep是静态方法,所以哪一个线程执行了含有sleep方法的代码,哪一个线程就会睡眠

考虑以下代码是哪一个线程睡眠了?

public class MyThread2 extends Thread {

    //自定义线程
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println(i);
        }
    }

    //主线程
    public static void main(String[] args) throws InterruptedException {
        MyThread2 thread2 = new MyThread2();
        thread2.sleep(5 * 1000);
        thread2.setName("线程-A");
        thread2.start();    
    }
}

“thread2.sleep(5 * 1000);”是在主线程中执行,所以主线程会被睡眠

public class MyThread2 extends Thread {

    //自定义线程
    public void run() {

        try {
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<100;i++){
            System.out.println(i);
        }
    }

    //主线程
    public static void main(String[] args) throws InterruptedException {
        MyThread2 thread2 = new MyThread2();
        thread2.start();    
    }
}

“Thread.sleep(2 * 1000);”是在自定义线程中执行,所以自定义线程会被睡眠
3. 得到当前线程对象
静态方法,哪个线程执行了含有currentThread()的代码,就返回哪个线程的对象

public class MyThread2 extends Thread {

    public MyThread2(String name) {

        super(name);
    }
    //自定义线程
    public void run() {
        System.out.println("this:"+this);
        System.out.println("2:"+Thread.currentThread());
    }

    //主线程
    public static void main(String[] args) throws InterruptedException {
        MyThread2 thread2 = new MyThread2("线程-B");
        thread2.start();    
        System.out.println("1:"+thread2.currentThread());
    }
}

运行后控制台输出如下:


1:Thread[main,5,main]
this:Thread[线程-B,5,main]
2:Thread[线程-B,5,main]
4. 设置线程的优先级
优先级越高的线程获取CPU资源的机率越大

//设置优先级1~10之间,数值越大优先级越高,默认的优先级为5
thread2.setPriority(10);

优先级越高的线程不会100%优先执行,只是优先执行的概率更大而已
5. isAlive()
isAlive()是用来判断线程的状态,当线程处于就绪、临时阻塞、运行状态,则返回true;如果线程处于新建并且没有启动的状态,或者线程已经执行结束,则返回false.

4. 线程的生命周期

线程生命周期

  • 新建状态:new Thread()后,线程就进入了新建状态
  • 调用start()方法启动线程后,线程就会进入可运行状态<此时线程是可以运行的,但是还没有真正的运行,只有等待CPU为其分配资源后线程才开始运行>
  • 临时阻塞状态,当线程执行了sleep或者wait方法,就会进入临时阻塞状态。
  • 死亡状态(结束状态):如果一个线程执行完了run()方法,就进入结束状态。

5. 线程安全问题

单一线程时,它只能在同一时间进行一项操作,所以永远不必担心有两个实体同时使用相同的资源。但是进入多线程环境后,它们就不再是孤立的,可能多个线程试图在同一时间访问同一个资源。比如两个线程同时从一个银行账户取款!

举一个多线程中常见的例子,三个售票窗口同时卖票,一共50张票,使用以下代码模拟:

class SaleTicket extends Thread {

    // 设置为静态变量,因为是三个线程的贡献资源数据
    static int num = 50;

    public SaleTicket(String name) {
        super(name);
    }

    @Override
    public void run() {

        while (true) {

            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "售出了"
                        + num + "号票");
                num--;
            } else {
                System.out.println("售罄");
                break;
            }
        }
    }
}

public class Demo1 {

    public static void main(String[] args) {
        SaleTicket a = new SaleTicket("A窗口");
        SaleTicket b = new SaleTicket("B窗口");
        SaleTicket c = new SaleTicket("C窗口");
        a.start();
        b.start();
        c.start();
    }
}

控制台输出如下:

A窗口售出了50号票
B窗口售出了50号票
B窗口售出了48号票
...
...
...

可以发现50号票被A、B窗口都卖出了,显然是错误的。这里就出现了线程安全问题,分析如下:
线程安全问题
由控制台的输出我们可以做以下假设:
假设A窗口先抢到了CPU的资源,在第5行判断num>0,打印第6行的内容,此时CPU的资源被B窗口抢走,注意A窗口并没有执行num–,然后B窗口在第5行判断num>0,打印第6行的内容,执行num–,此时num等于49,然后CPU的资源被A窗口抢走,此时A窗口应该执行第6行代码,num–。现在num = 48,B窗口抢夺了CPU的资源,判断num>0,执行第6行,就会打印出”B窗口售出了48号票”。

在什么情况下会出现线程安全问题?
**

  1. 存在多线程
  2. 存在共享的资源(比如上例中的票)
  3. 存在多个任务来操作共享资源,当前任务进行到一半时,CPU的资源被别的线程抢夺

**

线程安全问题的解决办法:
同步机制:调用synchronized方法时对象会被锁定,不能被其他的线程访问,除非当前线程完成了被同步的任务,并解除锁定。
①:同步代码块

synchronized("锁对象"){
    //需要被同步的代码
}

同步代码块要注意的事项:
- “锁对象”可以是任意的一个对象
- 多线程操作的锁对象必须是唯一的、共享的
- 在同步代码块中调用sleep方法,并不会释放锁
- 只有存在线程安全问题的时候才使用同步代码块,否则会降低线程执行的效率

使用同步代码块完成卖票,并解决线程安全问题:

class SaleTicket extends Thread {

    // 设置为静态变量,因为是三个线程的贡献资源数据
    static int num = 50;
    static Object o = new Object();

    public SaleTicket(String name) {
        super(name);
    }

    @Override
    public void run() {

        while (true) {
            // 同步代码块
            synchronized (o) {

                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "售出了"
                            + num + "号票");
                    num--;
                } else {
                    System.out.println("售罄");
                    break;
                }
            }
        }
    }
}

public class Demo1 {

    public static void main(String[] args) {
        SaleTicket a = new SaleTicket("A窗口");
        SaleTicket b = new SaleTicket("B窗口");
        SaleTicket c = new SaleTicket("C窗口");
        a.start();
        b.start();
        c.start();
    }
}

②:同步函数
- 使用synchronized 修饰函数
- 如果是非静态的同步函数,锁对象是当前对象;如果是静态的同步函数,锁对象是当前函数所属的类的class对象。


考虑以下代码能不能解决线程安全的问题?

class BankThread extends Thread {

    static int count = 5000;

    public BankThread(String name) {
        super(name);
    }

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

            if (count > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "取走了1000元,还剩余" + (count - 1000) + "元");
                count = count - 1000;
            } else {
                System.out.println("账户余额不足...");
                break;
            }

        }
    }
}

public class Demo2 {

    public static void main(String[] args) {

        BankThread grandFather = new BankThread("爷爷");
        BankThread grandMother = new BankThread("奶奶");
        grandMother.start();
        grandFather.start();
    }

}

控制台输出如下:

爷爷取走了1000元,还剩余4000元
奶奶取走了1000元,还剩余4000元
奶奶取走了1000元,还剩余2000元
爷爷取走了1000元,还剩余1000元
奶奶取走了1000元,还剩余0元
账户余额不足...
爷爷取走了1000元,还剩余-1000元
账户余额不足...

因为synchronized 修饰的函数是非静态的,所以当线程grandMother执行取钱任务时,锁对象是grandMother;当grandFather 执行取钱任务时,锁对象是grandFather 对象。锁对象不是唯一的就不能解决线程安全问题

推荐使用同步代码块解决线程安全问题!!!

6. 死锁

有时两个或多个线程需要锁定几个共享对象。这时可能引起死锁(deadlock) ,也就是说,每个线程已经锁定一个对象,正在等待锁定另一个对象。考虑一种有两个线程和两个对象的情形。线程1已经锁定了 object1 ,线程2锁定了 object2 。现在线程1等待锁定 object2 ,线程2等待锁定object1 。每个线程都等待另一个线程释放自己需要的资源,结果导致两个线程都无法继续运行。
例如老公拿着银行卡,但是没有密码;老婆记着密码,但是没有银行卡,参考以下代码:

class DeadLock extends Thread {

    public DeadLock(String name) {
        super(name);
    }

    @Override
    public void run() {
        if ("老公".endsWith(Thread.currentThread().getName())) {

            synchronized ("银行卡") {
                System.out.println("老公拿到了银行卡,等待密码...");
                synchronized ("密码") {
                    System.out.println("老公拿到了密码...");
                    System.out.println("老公可以去银行取钱了!!!");
                }
            }

        } else if ("老婆".endsWith(Thread.currentThread().getName())) {
            synchronized ("密码") {
                System.out.println("老婆拿到了密码,等待银行卡...");
                synchronized ("银行卡") {
                    System.out.println("老婆拿到了银行卡...");
                    System.out.println("老婆可以去银行取钱了!!!");
                }
            }
        }
    }
}

public class Demo3 {

    public static void main(String[] args) {
        DeadLock thread1 = new DeadLock("老公");
        DeadLock thread2 = new DeadLock("老婆");
        thread1.start();
        thread2.start();
    }
}

控制台输出如下:

老公拿到了银行卡,等待密码...
老婆拿到了密码,等待银行卡...

老公进入“银行卡”的同步代码块中,但是此时老婆进入了“密码”的同步代码块中。两个线程都会陷入无休止的相互等待状态。尽管这种情况并非经常出现,但一旦碰见,程序的调试就会变得异常艰难。就java语言本身来说,尚未提供防止死锁的措施,我们需要谨慎设计来避免。

但是上述例子中,只要“老公”线程跑的足够快,两者都是可以取到钱的!嘿嘿….
可以考虑将代码更改为如下,“老公”和“老婆”就有更大的可能性都能取到钱了!

else if ("老婆".endsWith(Thread.currentThread().getName())) {
            try {
                Thread.sleep(5*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized ("密码") {
                System.out.println("老婆拿到了密码,等待银行卡...");
                synchronized ("银行卡") {
                    System.out.println("老婆拿到了银行卡...");
                    System.out.println("老婆可以去银行取钱了!!!");
                }
            }
        }

7. 线程间通信

wait():等待,线程执行了wait()方法,就会进入等待状态,等待状态的线程必须要被其他线程调用notify()方法才能唤醒。
notify():唤醒,唤醒等待的线程。
Note:
- wait()和notify()属于Object对象的方法
- wait()和notify()必须要在同步代码块或同步函数中才能使用
- wait()和notify()必须要由锁对象调用

当i为偶数时生产者生产苹果,i为奇数是生产者生产香蕉;生产者生产一个产品消费者消费一个产品。使用以下代码进行模拟:

//产品类
class Product {
    boolean flag = false;// 产品是否已经存在
    String name;
    double price;
}

// 生产者
class Producer extends Thread {

    Product p;

    public Producer(Product p) {
        this.p = p;
    }

    // 不断地生产
    @Override
    public void run() {
        int i = 0;

        while (true) {
            synchronized (p) {
                if (p.flag == false) {
                    if (i % 2 == 0) {
                        p.name = "苹果";
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        p.price = 5.0;
                    } else {
                        p.name = "香蕉";
                        p.price = 2.5;
                    }
                    System.out.println("生产者生产了:" + p.name + "价格是:" + p.price);
                    p.flag = true;
                    //生产完毕,唤醒消费者
                    p.notify();
                    i++;
                } else {
                    try {
                        // 生产者已经生产完毕,等待消费者去消费
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

// 消费者
class Customer extends Thread {

    Product p;

    public Customer(Product p) {
        this.p = p;
    }

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

                if (p.flag == true) {
                    System.out.println("消费者消费了:" + p.name + "价格是:" + p.price);
                    p.flag = false;
                    //消费完了,唤醒生产者
                    p.notify();
                }else{
                    try {
                        //产品没有被生产,等待生产者生产
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public class Demo4 {

    public static void main(String[] args) {

        Product p = new Product();// 产品
        Producer producer = new Producer(p);// 生产者
        Customer customer = new Customer(p);// 消费者
        producer.start();
        customer.start();
    }

}

控制台输出:

生产者生产了:苹果价格是:5.0
消费者消费了:苹果价格是:5.0
生产者生产了:香蕉价格是:2.5
消费者消费了:香蕉价格是:2.5
...
...

说明:
1. 将p作为构造函数的参数传入,以实现p的共享;并且让p作为锁对象,调用wait和notify
2. 如果一个线程执行了wait()方法,那么该线程就会进入一个以”锁对象”为标识的线程池中并处于等待状态
3. 调用wait()方法会释放锁对象
4. 如果一个线程执行了notify()方法,那么就会唤醒上述线程池中的一个处于等待状态的线程
5. 调用notify()方法不能指定线程来唤醒,一般来说,先等待的线程先被唤醒

8. 停止线程

使用Thread类的stop()方法来停止一个线程,但此方法已经过时,不推荐使用。


如果需要停止一个处于等待状态的线程,可以通过布尔变量配合notify()或者interrupt()方法。

public class Demo5 extends Thread {

    boolean falg = true;

    public Demo5(String name) {
        super(name);
    }

    @Override
    public synchronized void run() {
        int i = 0;
        while (falg) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + i++);
        }
    }

    public static void main(String[] args) {

        Demo5 d = new Demo5("线程A");
        d.setPriority(10);
        d.start();

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);

            // 主线程i=80是,停止线程A
            if (i == 80) {
                d.falg = false;
                /*同步代码块*/
//              synchronized (d) {
//                  d.notify();
//              }

                /*使用interrupt清除等待状态,中断线程*/
                d.interrupt();
            }
        }
    }
}

线程A开启后,flag = true ,线程A会进入等待状态。然后执行主线程,当i=80,flag赋值为false,并且唤醒线程A;因为此时的flag为false,所以run方法里的代码会执行完毕,线程就被停止。


使用interrupt 强制清除处于等待状态的线程:如果线程在调用Object类的wait()、wait(long)或者wait(long,int)方法,或者Thread类的join()、join(long)、join(long,int)、sleep(long)或者sleep(long,int)方法后,执行interrupt会强制清除这些线程!并返回一个InterruptedException的异常。interrupt还可以指定清除哪个线程。

9. 守护线程和join()

守护线程的作用是在程序运行期间于后台提供一种“常规”服务,但是它并不属于程序的一个基本部分。一旦所有的非守护线程完成,程序就会终止,守护线程也会终止。可以调用isDaemon()查看一个线程是否为守护线程。线程默认不是守护线程,可以使用setDaemon(true)设置一个线程为守护线程。

以下代码为模拟软件更新包的后台下载,

public class Demo6 extends Thread {

    public Demo6(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("更新包目前下载" + i + "%....");
            if (i == 100) {
                System.out.println("更新包下载完毕,准备安装...");
            }
        }
    }

    public static void main(String[] args) {

        Demo6 d = new Demo6("后台线程");
        //设置为守护线程
        d.setDaemon(true);
        System.out.println(d.isDaemon());
        d.start();

        for(int j = 0;j<100;j++){
            System.out.println(Thread.currentThread().getName()+":"+j);
        }
    }
}

可以发现,当主线程停止时,后台下载更新的守护线程也会停止。



join():一个线程如果执行了join语句,那么就有新的线程加入,执行该语句的线程必须要让步给新加入的线程来完成任务,然后才能继续执行。

class Mom extends Thread{

    @Override
    public void run() {
        System.out.println("妈妈开始做饭");
        System.out.println("妈妈开始洗菜,切菜,炒菜...");
        System.out.println("妈妈发现没有酱油了...让我去打酱油");
        //我去打酱油
        Me me = new Me();
        me.start();
        try {
            /**
             * mom执行了me.join()语句,me线程就加入到mom线程,并且mom线程会等待me线程执行完毕才会继续执行
             * */
            me.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("妈妈继续做饭");
        System.out.println("全家人开心地吃晚饭...");
    }
}

class Me extends Thread{

    @Override
    public void run() {
        System.out.println("我一直往小卖铺走");
        System.out.println("打完酱油...");
        System.out.println("回家,并把酱油交给妈妈...");
    }
}

public class Demo7 {

    public static void main(String[] args) {

        Mom mom = new Mom();
        mom.start();
    }
}

控制台输出如下:

妈妈开始做饭
妈妈开始洗菜,切菜,炒菜...
妈妈发现没有酱油了...让我去打酱油
我一直往小卖铺走
打完酱油...
回家,并把酱油交给妈妈...
妈妈继续做饭
全家人开心地吃晚饭...



最后,希望这篇线程相关的文章可以对java的初学者有所帮助,上文有不当的地方,请大家提出自己宝贵的意见!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章