java 多线程

  在了解线程之前,要先了解进程。进程,就是正在运行中的程序。比如双击QQ图标, 打开QQ,和别人进行聊天,就开启了QQ进程。创建进程,,聊天执行的就是线程。线程就是进程中控制程序执行的一个控制单元。线程只是一片内存中的空间,它不负责程序执行。负责程序执行的是线程。一个进程中可以有多个线程,这就叫多线程。360安全卫士就是一个多线程。当我们启动360以后,它就会在内存中开发辟一片空间,Windows 资源管理器中多了360,但它什么都没有做,仅仅是启动了一个程序。 当我点击立即体验时,可以看到正在体验,这时再点击电脑清理,它会发现电脑正在进行清理,这两个功能在同时执行,但是都在一个程序中,这两个功能就是线程,我们可以同时启动多个线程。线程是负责程序执行。多线程的启动,就是为了运行各自的内容,这个内容可以称之为多线程执行的任务。

  多线程,其实是cpu在各个程序的各个线程中进行快速的切换,执行代码,由于切换非常快,给我们错觉是各个程序在同时执行。如果太多的多线程在同时执行,cpu在各个线程中来回切换的频率就会变小,电脑有时就很卡。 一个解决办法,就是增加cpu,这就是电脑2核,4核的由来。一个核执行一个程序,那就是真正同时运行。但是如果开多个程序,还是有问题。因为一个是内存不足,二是,线程切换确实消耗资源。这也是多线程的弊端。

   java 中创建多线程方法1: 定义一个子类继承Thread,复写其run 方法,写线程要执行的任务。在main函数中,创建子类的对象,调start方法开启线程,但这种方法几乎不用

   创建多线程方法2:实现Runnable 接口,覆写其run方法,多线程要执行的代码写在run中。在main函数中,分别创建Runnable接口的子类对象和线程对象,然后把子类对象传给线程对象, 线程对象调用start方法,开启线程。 

class PrintNumber implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(i);
        } 
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        PrintNumber pn = new PrintNumber();
        Thread t1 = new Thread(pn); // 几个线程对象,就开启几条线程,每个线程都调用pn对象的run方法,只有一个任务对象pn
        Thread t2 = new Thread(pn);
        t1.start();
        t2.start();
    }
}

   在继续学习多线程之前,要先了解一下线程的生命周期, 一般来说,线程有5种状态(state): new(创建), runnable(可运行状态), running(正在运行的状态),blocked(阻塞状态), dead(死亡状态)

  1,new state:  执行 Thread t1 = new Thread(d); 创建一个线程对象,线程进入创建阶段。但这时线程并没有被安排执行(not yet scheduled for running), 因此在这种情下,只能做两件事情: 或安排它执行(调用start() 方法),或直接杀死它(调用stop() 方法)。 调用start()方法,线程进入runnable 状态。注意,创建状态下,不能调用其它方法,否则会抛出异常。

  2,runnable:可执行状态,当线程进入到runable 状态,它就可以执行了,然后等待cpu 来执行它,也可以说,线程进入线程队列中,等待执行。如果这个线程和其它线程一样,有相同的优先级,它就会被随机分配时间来执行,所有的线程都会轮流获取到时间来执行。The runnable state means that the thread is ready for execution and is waiting for the availability of the processor. That is, the thread has joined the queue of threads that are waiting for execution. If all threads are of equal priority, then they are given time slots for execution in round robin fashion(轮流).

  3, running:正在运行的状态,CPU正在执行这个线程。线程在这个阶段,可以放弃它的执行,然后再加入到线程队列中,等待执行,也就是说进入到了runable 状态。The thread that relinquishes(放弃) control joins the queue at the end and again waits for its run. However, if we want a thread to relinquish control to another thread of equal priority before its turn comes, it can do so by invoking the yield() method.

  4,blocked:阻塞状态,为什么会出现这种情况呢?可能它需要等待其它线程的结果,暂时不具备执行状态,更不可能被执行。注意,blocked 状态不是dead 状态,它完全具备重新执行的能力。A thread is said to be blocked when it is prevented form entering into the runnable state and subsequently the running state. This happens when the thread is suspended, sleeping or waiting in order to satisfy certain requirements. A blocked thread is considered ‘not ruunable ’ but not dead and therefore fully qualified to run again.

  5, dead 死亡状态,当一个线程执行完之后,它自然进入到死亡状态,因为它的使命已经完成了。当然,我们也可以直接杀死它。不管是在创建的候,还是在运行的时候,甚至在阻塞的时候。However, we kill it by sending the stop message to it at any state thus causing a premature death. A thread can be killed as soon as it is born, or while it is running, or even when it is in ‘not runnable’(blocked condition)

  线程安全和线程同步

  Because multithreading introduces an asynchronous behavior to your programs, there must be a way for you to enforce synchronicity when you need it. For example, if you want two threads to communicate and share a complicated data structure, such as a linked list, you need some way to ensure that they don’t conflict with each other. That is, you must prevent one thread from writing data while another thread is in the middle of reading it.

class Ticket implements Runnable {
    private int num = 100;

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

public class Main {
    public static void main(String[] args) {
        Ticket ticket = new Ticket(); // 一个线程任务对象
        
        Thread t1 = new Thread(ticket); // 每一个线程都执行ticket的run方法
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        Thread t4 = new Thread(ticket);
        
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

  执行上面的程序,出现了-1,-2,这就是线程安全问题。线程安全问题的原因在于,多条线程共享数据,就是run 方法中访问了外部的变量,并对外部的变量进行了改变。比如卖票中,run 方法肯定要访问这个变量,并对其进行修改,进行-- 操作,因为卖票卖一张少一张。这时就有问题了。因为代码是一行一行进行执行的,如果一个线程进入到了一个run 方法中,但只到达了第一个行,就不执行了。然后第二个线程进入run 方法,执行完了,这时第一个线程就开始执行,执行run方法中的第二条,System.out. 它再执行num-- 操作,如果第二个线程执行完时,num ==0 了,这时第一个线程再执行num -- 时就会出现 -1 的结果,这时就会有问题了,因为买票不可以有-1张。这就是线程安全问题。这了引出了线程安全问题的第二个原因,线程的执行任务run 方法,它有多条执行语句,但却不是一个整体,不能作为一个整体进行执行  

  解决的方法也就简单了,就是让多条代码语句封装起来,形成一个整体,当有一个线程在执行这些代码的时候,其它线程不可以进行操作,只有当一个线程执行完毕后,其它线程才能执行, 这就是所说的同步代码块,把run方法中多条代码用{} 封装起来,然后加一个关键字synchronized。当两个或多个线程需要访问共享资源时,它们需要某种方法来确保该资源一次仅由一个线程使用。实现这一点的过程称为同步。同步的关键是监视器。一个监视器是一个对象,被当作互斥锁使用。在给定时间只有一个线程可以拥有监视器。

class Ticket implements Runnable {
    private int num = 100000;
    private Object obj = new Object();

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

  加了同步代码块以后,也带来了弊端,就是,每一个线程在进入同步代码块之前都要先进行判断,降低了效率。如果run方法中的代码是调用的某个对象的方法,可以对该方法用synchronized修饰,函数的返回值前面添加synchronized,函数也称为同步函数。

class Ticket implements Runnable {
    private int num = 100000;
    private Object obj = new Object();

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

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

  同步函数也是使用的锁机制,只不过,他使用的锁是this. 而同步代码块使用的锁是任意的对象。

  线程之间的通信(生产者和消费者)

  多条线程在处理同一资源,但是任务不同,比如,有的线程向内存中写,有的线程向内存中读。写了之后,要让读线程去读取内容,读了之后,再让写线程去写内容,这就需要等待(wait)和通知(notifiy),由于是处理同一资源,所以多条线程要使用同一个锁。wait(), notify(),notifyAll() 这些方法必须在同步代码块中使用,因为它们都是操作线程状态的方法,它们都是锁中的方法。

class Resource {
    private String name;
    private String sex;
    private boolean flag = true;
    
    public synchronized void set(String name, String sex) {
        if (flag) {
            try {
                this.wait();
            } catch(InterruptedException e) {
                System.out.println(e);
            }
        }
        this.name = name;
        this.sex = sex;
        
        flag =true;
        this.notify(); //锁是this
    }
    
    public synchronized void out() {
        if(!flag) {
            try {
                this.wait();
            } catch(InterruptedException e) {
                System.out.println(e);
            }
        }
        System.out.println(name + "..." + sex);
        flag = false;
        this.notify();
    }
}

class Input implements Runnable {
    Resource r;
    
    Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        int x = 0;
        
        while(true) {
            if(x == 0) {
                r.set("mike", "nan");
            } else {
                r.set("Lily", "nv");
            }
            
            x = (x+1) % 2;
        }
    }
}

class Output implements Runnable {
    Resource r;
    
    Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        
        while(true) {
            r.out();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Resource r = new Resource();
        
        Input in = new Input(r);
        Output out = new Output(r);

        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);

        t1.start();
        t2.start();
    }
}

   上面是单生产者和单消费者,但到了多生产者和多消费者又会出问题,在main函数中再创建生产者和消费者线程,

public class Main {
    public static void main(String[] args) {
        Resource r = new Resource();
        
        Input in = new Input(r);
        Output out = new Output(r);

        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        Thread t3 = new Thread(in);
        Thread t4 = new Thread(out);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

  当多个生产者线程开始执行,只有某一个线程会执行完毕,并将flag设为true,其它线程会在this.wait()处等待,当执行完毕的生产线程,会调用notify(), 但notify()只是唤醒多条线程中的任意一个线程,如果它唤醒的又是生产线程,那生产线程又会从this.wait()处执行,再次执行set操作,你会发现有问题,一个是生产的内容,并没有被消费,二是多生产了,所以要把if改成while,每一条线程都要重复判断flag是否是true, 执行到this.wait()后,如要唤醒,还是要判断flag,如果是true, 它什么都不干,不会进行set操作(生产动作)。 这时也会带来一个问题,死锁了。它没有唤醒其它进程,所以要进行notifyAll, 把进程中的其它线程都唤醒,总有一个消费进程被唤醒。多个消费进程也是同样的道理,它也要重复判断flag,要不然,一个消费唤醒另一个消费进程,重复消费生产出来的一个东西,改在while死锁,还要使用notifyAll() 唤醒所有进程。

class Resource {
    private String name;
    private String sex;
    private boolean flag = true;
    
    public synchronized void set(String name, String sex) {
        while (flag) {
            try {
                this.wait();
            } catch(InterruptedException e) {
                System.out.println(e);
            }
        }
        this.name = name;
        this.sex = sex;
        
        flag =true;
        this.notifyAll(); //锁是this
    }
    
    public synchronized void out() {
        while (!flag) {
            try {
                this.wait();
            } catch(InterruptedException e) {
                System.out.println(e);
            }
        }
        System.out.println(name + "..." + sex);
        flag = false;
        this.notifyAll();
    }
}

  但是notifyAll和while 性能有点低,所以 JDK又提供了java.uitls.concurrent.lock包,来操作锁,将同步和锁封装成了对象,能显示的操作锁,因为synchronized是隐式的操作锁(进入代码块,就拿到锁,出去代码块,就释放锁,你自己不能控制),显示操作锁(创建锁对象,获取锁,释放锁,自己控制),控制更为精细。既然能显示创建锁,锁上的wait, notity 的使用也发生了变化, 它们到了Condition对象上,通过锁,获取到condition对象,再使用await(), signal(),  需要注意的是,一个锁,可以有多个condition 对象。因此,lock对象替代了synchronized方法和语句,Condition替换了Object对象上的wait(),notify()方法。现在就可以通过锁,创建两个监视器,一个用于监视生产者,一个用于监视消费者,这样在唤醒线程的时候,可以指它唤醒生产者还是消费者

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Resource {
    private String name;
    private String sex;
    private boolean flag = true;

    private Lock lock = new ReentrantLock();
    private Condition producer_con = lock.newCondition();
    private Condition consumer_con = lock.newCondition();

    public void set(String name, String sex) {
        lock.lock();
        try {
            while (flag) {
                try {
                    producer_con.await(); // 生产者线程等待
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
            this.name = name;
            this.sex = sex;

            flag = true;
            consumer_con.signal(); // 唤醒一个消费者
        } finally {
            lock.unlock();
        }
    }

    public void out() {
        lock.lock();
        try {
            while (!flag) {
                try {
                    consumer_con.await(); // 消费者等待
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
            System.out.println(name + "..." + sex);
            flag = false;
            producer_con.signal(); // 
        } finally {
            lock.unlock();
        }
    }
}

   线程的 join 方法, 在main方法中,调用t1.join(),就表示,main线程要等待t1线程结束再执行,所以执行到t1.join()时,main线程就放弃执行资格和执行权,让t1线程执行,直到t1线程执完成,main线程再执行。This method waits until the thread on which it is called terminates. Its name comes from the concept of the calling thread waiting until the specified thread joins it.

posted @ 2017-12-24 22:40  SamWeb  阅读(265)  评论(0编辑  收藏  举报