多线程

当我们在使用电脑的时候,一个应用程序就是一个进程,比如QQ、Google浏览器,但是当我们使用QQ时,是可以一边视频聊天,然后一边和另一个人文字聊天的,使用Google的Chrome浏览器的时候,同样也是可以同时做多件事,比如一边下载文档资料,一边继续浏览网页;这样一个程序可以同时做多件事的原因,就是因为一个进程(应用程序)里面是有多个线程在同时运行的,而且现在的电脑都是多核的,因此使用多个线程可以让程序运行更有效率,更能发挥CPU的性能。

这篇文章就主要想写一下多线程,先看一下多线程和单线程之间的区别:

单线程:安全性高,但效率低
多线程:效率高,但安全性相对于低

单线程因为至始至终都只有一个线程在运行,因此不会有线程互相抢占资源,也就不会有线程之间因为相互运行而发生错误,所以能够保证线程运行时的安全性,但是因为线程数只有一个,所以和多线程将一个任务分配给多个线程相比,运行的速度还是要差一点,效率来的要低一点;而多线程程序刚好相反,因为多个线程可以同时运行,完成任务的效率肯定要高一点,但是可能因为线程间会抢占资源,在这个过程中可能会引发错误,因此安全性来的要低一点。

1.线程实现方式

多线程的实现方式一共有两种,第一种是写一个类,继承 Thread 类,并在自己写的类中重写Thread 类当中的 run 方法;第二种方式就是,自己写一个类,实现Runnable接口,并重写接口当中的 run 方法。

a.继承Thread类,并在类中重写`Thread` 类当中的run方法
b.实现`Runnable`接口,并重写接口当中的run方法

1.1 继承类方式

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName());
            System.out.println(this.getName() + ":" + i);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.setName("鸣人");
        t.start();

        MyThread t2 = new MyThread();
        t2.setName("佐助");
        t2.start();
    }
}

1.2 实现接口方式

public class MyThread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            String name = Thread.currentThread().getName();
            System.out.println(name + ":" + i);
        }
    }
}

public class MyThread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            String name = Thread.currentThread().getName();
            System.out.println(name + ":" + i);
        }
    }
}

类方式和接口方式都可以实现多线程,只是需要注意一点的就是,因为Java中只支持单继承,因此一个类在继承了一个类之后就不能再继承其它的类了,所以在明确了某个需要实现多线程的类还要继承别的类的话,那就要使用接口方式实现多线程了。

2.可能出现的问题

直接像上面使用多线程编程可能出现问题,举个例子说明一下:

class TicketThread implements Runnable {
    int tickets = 100;// 火车票数量

    @Override
    public void run() {
        // 出售火车票
        while (true) {
            // 当火车票小于0张,则停止售票
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 得到当前的线程名称
                String name = Thread.currentThread().getName();
                System.out.println(name + ":" + tickets--);
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建线程对象
        TicketThread tt = new TicketThread();

        Thread t1 = new Thread(tt);
        t1.setName("窗口1");
        Thread t2 = new Thread(tt);
        t2.setName("窗口2");
        Thread t3 = new Thread(tt);
        t3.setName("窗口3");

        // 启动线程对象
        t1.start();
        t2.start();
        t3.start();
    }
}

上面的程序执行的时候有可能会执行到-1,那显然就是出现错误了,那是为什么呢?就是因为使用了多线程,在执行 System.out.println(name + ":" + tickets--); 这一句的时候,当tickets已经等于1的时候,有可能一个程序执行到这里的时候,还没有执行完,但是这时候程序的执行权却被另一个线程夺走了,另一个线程也执行了这一句,那tickets就变成0了,而导致被夺走执行权的线程再次拿到执行权的时候,它还是会执行这一句,所以tickets就变成-1了。

3.解决问题

问题发生的原因就是因为在某一时间点某段代码只能被一个线程访问执行,但是上面的代码却有可能被另一个线程也执行,这就引发了错误。

其实解决这个问题也比较简单,那就是给某一时间点只能被一个线程访问的代码加上一把锁,它在执行的时候锁是锁上的,等它执行完了之后再把锁打开,Java中就是用同步关键字 synchronized 来实现这一点的,当某一个线程需要访问某个共享数据的时候,先给需要共享的数据上锁,上锁之后,其他的线程不能操作,用完之后释放锁对象,其他线程就又可以进行操作了

public class MyRunnable implements Runnable {
    int tickets = 100;

    String s = "dcdd";

    @Override
    public void run() {
        while (true) {
                method();
        }
    }

    public synchronized void method() {
        if (tickets > 0) {
            try {
                Thread.currentThread().sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":"
                    + (tickets--));
        }
    }
}

public class RunnableDemo2 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();

        Thread t = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);

        t.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t.start();
        t2.start();
        t3.start();
    }
}

下面的这个程序运行就是正确的了,因为为共享的数据上了锁,某一时间点只允许一个线程访问。

4.应用的小例子

4.1 领取电影票

一共有1000张电影票,可以在两个窗口领取
要求:请用多线程模拟卖票过程并打印剩余电影票的数量

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

        MyThread r = new MyThread();

        Thread t = new Thread(r);
        Thread t2 = new Thread(r);

        t.setName("窗口1");
        t2.setName("窗口2");

        t.start();
        t2.start();
    }
}

class MyThread implements Runnable {
    int i = 1000;
    Object s = new Object();

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

                if (i > 0) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "还有"
                            + (i--) + "张电影票");
                }
            }
        }
    }
}

4.2 发放礼物

有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来.

public class Test2 {
    public static void main(String[] args) {
        MyThread2 r = new MyThread2();

        Thread t = new Thread(r);
        Thread t2 = new Thread(r);

        t.setName("圣诞老人");
        t2.setName("圣诞婆婆");

        t.start();
        t2.start();
    }
}

class MyThread2 implements Runnable {
    int gifts = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (gifts > 10) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String name = Thread.currentThread().getName();
                    System.out.println(name + ":" + (gifts--));
                }
            }
        }
    }
}

4.3 输出1到100间的数字

同时开启两个线程,共同输出1-100之间的所有数字,并且将输出奇数的线程名称打印出来

public class Test3 {
    public static void main(String[] args) {
        MyThread3 r = new MyThread3();

        Thread t = new Thread(r);
        Thread t2 = new Thread(r);

        t.setName("黑猫");
        t2.setName("白猫");

        t.start();
        t2.start();
    }
}

class MyThread3 implements Runnable {

    int num = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (num > 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String name = Thread.currentThread().getName();
                    if (num % 2 == 1) {
                        System.out.println(name + ":" + (num--));
                    }else{
                        System.out.println(num--);
                    }
                }
            }
        }
    }
}

4.4 三个线程分别打印各自范围内的数字

同时开启三个线程,输出1到30的数据

要求:
线程1,输出 1-10
线程2,输出 11-20
线程3,输出 21-30

代码:

public class Test4 {
    public static void main(String[] args) {
        MyThread4 r = new MyThread4(1, 10);
        Thread t = new Thread(r);

        MyThread4 r2 = new MyThread4(11, 20);
        Thread t2 = new Thread(r2);

        MyThread4 r3 = new MyThread4(21, 30);
        Thread t3 = new Thread(r3);

        t.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");

        t.start();
        t2.start();
        t3.start();
    }
}

class MyThread4 implements Runnable {

    Object obj = new Object();

    int begin;
    int end;

    public MyThread4(int begin, int end) {
        super();
        this.begin = begin;
        this.end = end;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (end >= begin) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String name = Thread.currentThread().getName();
                    System.out.println(name + ":" + (begin++));
                }
            }
        }
    }
}

5.总结

感觉自己对多线程还只是了解到了一点皮毛,还有很大部分的内容需要学习,最关键的大概还是会去运用吧,只要能够运用了就不会感觉那么深奥了,以后也应该多找这方面的书看一下,加深对这方面的理解。

坚持原创技术分享,您的支持将鼓励我继续创作!