梁恒嘉的技术专栏 全栈之路

Java中的线程


文章讲述线程相关知识。包括进程,线程定义,线程的创建过程,线程常见方法,线程的生命周期,线程安全问题,线程间通信,生产者消费者问题,线程卖票程序

一、预备知识

程序:程序是指完成特定任务,用某种语言编写的一组指令集合。指一段静态的代码。

进程:正在运行的程序。是操作系统资源分配的最小单位。

线程:进程进一步细分为线程,是一个进程内部最小执行单元。是操作系统任务调度的最小单元。可以独立运行,不可脱离进程。

进程中可以包含多个线程,多个线程共享同一个进程的内存资源。进程必须包含一个线程(主线程),在主线程中可以创建并启动其它线程。线程不可以脱离进程运行。


二、线程的相关知识

1.继承Thread类

代码如下:

public class ThreadDemo extends Thread{
    @Override
    public void run() {
        super.run();
        //继承Thread类,重写润方法,在run方法里面添加任务代码
        System.out.println(Thread.currentThread().getName()+"正在运行...");

    }
}

2.实现Runnable接口

代码如下:

public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        //实现Runnable接口,重写run方法
        System.out.println(Thread.currentThread().getName()+"正在运行...");

    }
}

3.实现Callable接口

这种方式创建线程,线程执行完毕可以带返回值,并且支持泛型,自定义返回值类型。也支持异常,扩展性更强。

代码如下:

public class CallableThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 5; i++) {
            sum += i;
        }
        return sum;
    }
}


public class CallableTest {
    public static void main(String[] args) {
        CallableThread callableThread = new CallableThread();
        FutureTask<Integer> task = new FutureTask<>(callableThread);
        Thread thread = new Thread(task);
        thread.start();

        try {
            Integer sum = task.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4.开启线程

代码如下:

public class Test {
    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();

        RunnableDemo runnableDemo = new RunnableDemo();
        Thread thread = new Thread(runnableDemo);
        thread.start();
    }
}

5.线程中的常用方法

代码如下:

		//run()方法是执行线程任务
        //start()方法是开启一个线程
        //setName()给线程设置名字,也可以通过Thread类的构造器设置名字
        //Thread.currentThread()获取当前正在运行的线程
        //getName()获取线程的名字
        //线程一共有10级,1-10,等级越高,优先权越大,越容易被CPU调度,线程默认的优先权是5
        //setPriority()设置线程的优先权
        //getPriority()获取线程的优先权

6.加入线程后的TCP聊天程序

代码如下:

public class SendThread extends Thread {
    private Socket socket;

    public SendThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"正在执行:");
        try {
            OutputStream out = socket.getOutputStream();
            DataOutputStream dout = new DataOutputStream(out);
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String msg = scanner.next();
                dout.writeUTF(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


public class ReceiverThread extends Thread{
    private Socket socket;

    public ReceiverThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"正在执行:");
        try {
            InputStream in = socket.getInputStream();
            DataInputStream din = new DataInputStream(in);
            while(true){
                String msg = din.readUTF();
                System.out.println(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9966);
        Socket socket = serverSocket.accept();

        ReceiverThread receiverThread = new ReceiverThread(socket);
        receiverThread.setName("接收端线程");
        receiverThread.start();

        SendThread sendThread = new SendThread(socket);
        sendThread.setName("发送端线程");
        sendThread.start();
    }
}


public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 9966);
        SendThread sendThread = new SendThread(socket);
        sendThread.setName("发送端线程");
        sendThread.start();

        ReceiverThread receiverThread = new ReceiverThread(socket);
        receiverThread.setName("接收端线程");
        receiverThread.start();
    }
}

7.线程的生命周期图

线程的生命周期图 笔者使用自定义类继承Thread类,重写run()方法,创建自定义线程。

当新建一个自定义类对象的时候是线程的新建状态,表示已经有线程对象,但是这个线程无法获取CPU调度。

当调用start()方法后,线程进入就绪状态,表示这个线程有获取CPU调度的机会。

当线程获取到CPU调度,则线程处于运行状态,CPU执行时间片用完或者调用yield()方法,则线程再次进入就绪状态,等待CPU调度。

若线程在运行状态时程序调用sleep()方法,或wait()方法,或join()方法,或等待同步锁,则线程进入阻塞状态。

当sleep时间到,执行notify()/或notifyAll(),join结束,获取到同步锁,则线程进入就绪状态,等待CPU调度。

当线程将run()方法执行完,或调用stop()方法,或遇到ERROR/EXCEPTION且为处理,则线程消亡。


8.线程的分类

在Java里面将线程分为守护线程与用户线程。

守护线程:当程序中的所有用户线程都执行完毕,则守护线程也会结束。

代码如下:

ThreadDemo threadDemo = new ThreadDemo();
//将该线程设置为守护线程
threadDemo.setDaemon(true);
threadDemo.start();

三、多线程

程序需要同时执行多个任务。

程序需要实现一些需要等待的任务。

需要执行后台程序。

多线程提高程序响应。

提高CPU利用率。

改善程序结构。

多线程需要协调和管理,所以需要CPU时间跟踪线程。

多线程之间访问共享资源,会相互产生影响,必须解决竞用共享资源的问题。

并发:是指一个CPU在同一时间段内依次执行多个任务。

并行:是指多个CPU在同一时刻同时执行多个任务。

1.使用多线程模拟买票程序

我们模拟买票程序,以下程序是未加入同步机制的代码。

我们使用static变量num表示共享资源。num表示票的代号。

开启了两个线程对共享资源同时访问。

2.问题所在

此时票的数量是共享资源,所有的线程对象共同访问同一个票数量。但是我们CPU的执行权是由操作系统决定,我们无法控制CPU先来后到的执行线程。如:窗口1线程买到了票10,此时票的数量还未减1,变成9(下一次只可以卖出票9,不可以卖出票10,因为票10已经售出。),但是在这个时候窗口1线程CPU执行权被剥夺,窗口2获得CPU执行权。此时票10在下一次执行的时候,窗口2会以为票10存在,继续由窗口2线程买到票10。票10被重复卖出,出现问题。

使用Thread模拟代码如下:

public class ThreadDemo extends Thread {

    static int num = 10;

    @Override
    public void run() {
        while (true) {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "买到了票" + num);
                num--;
            } else {
                break;
            }
        }
    }
}


public class Test {
    public static void main(String[] args) {
        ThreadDemo threadDemo1 = new ThreadDemo();
        threadDemo1.setName("窗口1");
        threadDemo1.start();
        ThreadDemo threadDemo2 = new ThreadDemo();
        threadDemo2.setName("窗口2");
        threadDemo2.start();
    }
}

使用Runnable模拟代码如下:

public class RunnableDemo implements Runnable{
    int num = 10;

    @Override
    public void run() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (true){
            if (num> 0 ){
                System.out.println(Thread.currentThread().getName()+"买到了票"+num--);
            }else {
                break;
            }
        }
    }
}


public class RunnableTest {
    public static void main(String[] args) {
        RunnableDemo runnableDemo = new RunnableDemo();
        Thread thread1 = new Thread(runnableDemo, "窗口1");
        Thread thread2 = new Thread(runnableDemo, "窗口2");
        thread1.start();
        thread2.start();
    }
}

代码执行结果如下:

买票程序执行结果

3.解决重复卖出同一个票问题

我们的解决办法是:加入同步机制。

当多个线程对同一资源(共享资源)进行读写的时候,会引起错误,我们必须使用同步机制,使多个线程先来后到依次访问共享资源。同步就是加锁和排队。 使用锁的机制,我们给共享资源加入锁,当存在正在访问共享资源线程时,我们这个线程处于加锁状态,此时当其他线程访问共享资源的时候,会等待同步锁结束才可以访问同步资源。实现对共享资源的依次访问,同一时刻只会有一个线程读写共享资源。

使用Thread解决代码如下:


public class ThreadDemo extends Thread {

    static int num = 10;
    static Object obj  = new Object();

    @Override
    public void run() {
        while (true) {
            try {
            //这个地方加入sleep函数让我们的线程休眠1秒,使其他线程有机会获得CPU执行权,让我们的程序更直观的
            //表现出使用同步机制之后的效果
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //同步代码块如下,obj是一个同步对象(线程中只存在一份的对象),多个线程访问的是同一个对象
            //这个同步对象不可以是this,因为this代表的是当前对象,每个线程访问的时候,都是一个新的线程对象。
            //与同步对象的定义相悖。
            synchronized (obj){
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "买到了票" + num);
                    num--;
                } else {
                    break;
                }
            }
        }
    }
}


public class Test {
    public static void main(String[] args) {
        ThreadDemo threadDemo1 = new ThreadDemo();
        threadDemo1.setName("窗口1");
        threadDemo1.start();
        ThreadDemo threadDemo2 = new ThreadDemo();
        threadDemo2.setName("窗口2");
        threadDemo2.start();
    }
}

使用Runnable解决代码如下:


public class RunnableDemo implements Runnable {
    int num = 10;

    @Override
    public void run() {

        while (true) {
            synchronized (this) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "买到了票" + num--);
                } else {
                    break;
                }
            }
        }
    }
}



public class RunnableTest {
    public static void main(String[] args) {
        RunnableDemo runnableDemo = new RunnableDemo();
        Thread thread1 = new Thread(runnableDemo, "窗口1");
        Thread thread2 = new Thread(runnableDemo, "窗口2");
        thread1.start();
        thread2.start();
    }
}

代码执行结果如下:

加入同步机制之后的卖票代码

4.同步机制的影响

一个线程持有锁会使其他需要此锁的线程挂起;在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延迟,引起性能问题。

5.Lock(锁)

Lock是一个接口,我们使用ReentrantLock类进行实现。

我们可以使用ReentrantLock的实例进行显示的加锁和释放锁。

使用ReentrantLock给程序加锁,代码如下:

public class ThreadDemo extends Thread {

    static int num = 10;
    static Lock lock = new ReentrantLock();

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

            try {
                lock.lock();
                Thread.sleep(1000);
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "买到了票" + num);
                    num--;
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                lock.unlock();
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}


public class Test {
    public static void main(String[] args) {
        ThreadDemo threadDemo1 = new ThreadDemo();
        threadDemo1.setName("窗口1");
        threadDemo1.start();
        ThreadDemo threadDemo2 = new ThreadDemo();
        threadDemo2.setName("窗口2");
        threadDemo2.start();
    }
}

6.死锁

两个线程彼此等待对方的锁,造成程序无法向前执行的现象。

代码如下:

public class DieThread extends Thread{

    static Object objA = new Object();
    static Object objB = new Object();

    boolean flag;

    public DieThread(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (objA){
                System.out.println("if objA");
                synchronized (objB){
                    System.out.println("if objB");
                }
            }
        } else {
            synchronized (objB){
                System.out.println("else objB");
                synchronized (objA){
                    System.out.println("else objA");
                }
            }
        }
    }
}


public class DieThreadTest {
    public static void main(String[] args) {
        DieThread thread1 = new DieThread(true);
        thread1.start();
        DieThread thread2 = new DieThread(false);
        thread2.start();
    }
}

7.线程间通信

线程通信指的是多个线程相互牵制,相互调度,即线程间的相互作用。

涉及三个方法:

wait():让当前线程进入阻塞状态,并释放同步监视器。

notify():唤醒被wait的线程,如果有多个线程被wait,则唤醒优先级最高的线程。

notifyAll():唤醒所有被执行wait的线程。

这三个方法必须在同步代码块或者同步方法中使用。

代码如下:

public class CommunicationThread extends Thread {
    static int num = 1;
    static Object object = new Object();

    @Override
    public void run() {

        while (true) {
            synchronized (object) {
             	object.notifyAll();
                if (num==101){
                    break;
                }
       
                System.out.println(Thread.currentThread().getName() + ":" + num++);
                try {
                    Thread.sleep(100);
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}


public class Test {
    public static void main(String[] args) {
        CommunicationThread thread1 = new CommunicationThread();
        thread1.start();
        CommunicationThread thread2 = new CommunicationThread();
        thread2.start();
    }
}

运行结果如下:

运行结果

该程序会使用两个线程交替打印1~100的数字。

8.生产者与消费者问题

现在有一个柜台,一个消费者,一个生产者,当柜台没有商品的时候我们需要让生产者生产商品,当存在商品时我们需要消费者消费商品。

代码如下:

public class Counter {
    int num = 1;

    public synchronized void custom() {

        if (num > 0) {
            num--;
            System.out.println("消费者消费商品:" + num);
            this.notify();
        } else {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public synchronized void produce() {

        if (num == 0) {
            num++;
            System.out.println("生产者生产商品:" + num);
            this.notify();
        } else {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}


public class Custom extends Thread {
    Counter counter;

    public Custom(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            counter.custom();
        }
    }
}


public class Produce extends Thread {
    Counter counter;

    public Produce(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            counter.produce();
        }
    }
}


public class Test {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Custom custom = new Custom(counter);
        custom.start();
        Produce produce = new Produce(counter);
        produce.start();
    }
}

总结

为完成指定任务,使用特定语言编写的静态代码称之为程序。正在运行的程序称之为进程。CPU将资源分配给进程,进程之下是线程,所有线程共享进程的的CPU资源,线程是依赖于进程,是CPU调度的最小单元,线程可以独立运行。

线程的生命周期包括:创建,就绪,运行,阻塞,死亡。

引用多线程机制是为了提高CPU利用率更加快捷的执行程序,完成任务。

但是多线程并行访问共享资源的时候会存在冲突问题,可以使用同步机制解决。

同步机制就是排队和加锁,让线程依次执行,同一时刻只存在一个线程访问共享资源。

实际上是将共享资源转变成了临界资源。将并行的访问同一资源变成了并发的访问同一资源。


上一篇 Java网络编程

下一篇 Java中的GUI

 

Content