多线程

作者: luoqiang108 | 来源:发表于2018-07-13 10:23 被阅读0次
    • 进程:一个操作系统中可以同时运行多个任务(程序)。系统级别上的多线程(多个任务),每个任务就叫做一个进程。
    • 线程:一个程序同时可能运行多个任务。那么每个任务就叫做一个线程。
    • 并发:线程是并发运行的。操作系统将时间划分为若干个片段(时间片),尽可能的均匀分配给每一个任务,被分配时间片后,任务有机会被cpu所执行。随着cpu高效的运行,宏观上看所有任务都在运行。但微观上看,每个任务都是走走停停的。这种现象称之为并发。

    Thread类——线程类

    • thread类的实例代表一个并发任务。
      并发的任务逻辑是通过重写Thread的run方法实现的。
    • 线程调度:线程调度机制会将所有并发任务做统一的调度工作,划分时间片(可以被cpu执行的时间)给每一个任务,时间片尽可能均匀,但做不到绝对均匀。同时,被分配时间片后,该任务被cpu执行,但调度的过程中不能保证所有任务都是平均的获取时间片的次数。只能做到尽可能平均。这两个都是程序不可控的。
    /**
     * 线程
     *
     * 实现线程需要两步
     * 1:继承自Thread
     * 2:重写run方法
     * run方法中应该定义我们需要并发执行的任务逻辑
     */
    
    public class MyFirstThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(i);
            }
        }
    }
    
    public class MySecThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("你好"+i+"次");
            }
        }
    }
    
    • 另一种创建线程的方式,将线程与执行的逻辑分离开。因为有了这样的设计,才有了线程池。关注点在于要执行的逻辑。
    • Runnable接口:
      用于定义线程要执行的任务逻辑。我们定义一个类实现Runnable接口,这时我们必须重写run方法。在其中定义我们要执行的逻辑。之后将Runnable交给线程去执行。从而实现了线程与其执行的任务分离开。
    • 解耦:线程与线程体解耦。打断依赖关系
    public class MyFirstRunnable implements Runnable {
        /**
         * run方法中定义线程要执行的逻辑
         */
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(i);
            }
        }
    }
    
    public class MySecRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("你好"+i+"次");
            }
        }
    }
    
    /**
     * 测试线程
     */
    
    public class TestThread {
        public static void main(String[] args){
            /*测试并发操作多个任务*/
            Thread t1 = new MyFirstThread();
            Thread t2 = new MySecThread();
            /*启动线程开始并发执行任务
            * 注意!想并发操作不要直接调用run方法!而是调用线程的
            * start()方法启动线程。
            * */
            //t1.start();
            //t2.start();
            /*不要使用stop()方法来停止线程的运行。这是不安全的操作
            * 想让线程停止,应该通过run方法的执行完毕来进行自然结束
            * (可以通过加标志位的方式来让run方法提前结束来停止线程)。
            * */
            //t1.stop();
            Runnable r1 = new MyFirstRunnable();
            Runnable r2 = new MySecRunnable();
            /*将两个任务分别交给线程去并发处理
            * 将任务交给线程可以使用线程的重载构造方法
            * Thread(Runnable runnable)
            * */
            Thread thread1 = new Thread(r1);
            Thread thread2 = new Thread(r2);
            //thread1.start();
            //thread2.start();
            /*使用匿名内部类方式创建线程*/
            Thread td1 = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        System.out.println(i);
                    }
                }
            };
            //实现Runnable接口的形式
            Thread td2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        System.out.println("你好"+i+"次");
                    }
                }
            });
            td1.start();
            td2.start();
        }
    }
    

    线程休眠

    /**
     * 线程睡眠阻塞
     * 阻塞:
     *      使当前线程放弃cpu时间,进入阻塞状态。在阻塞状态的线程
     *      不会分配时间片。直到该线程结束阻塞状态回到Runnable状态
     *      方可再次获得时间片来让cpu运行(进入Running状态)
     */
    
    public class ThreadSleep {
        public static void main(String[] args){
            /*
            * 让当前线程主动进入阻塞状态
            * Thread.sleep(long time)
            * 主动进入阻塞状态time毫秒后回到Runnable状态
            * */
            int i = 0;
            while (true){
                System.out.println(i+"秒");
                i++;
                try {
                    /*
                    * 使用Thread.sleep()方法阻塞线程时强制让我们必须捕获“中断异常”
                    * 引发情况:
                    *       当前线程处于Sleep阻塞期间,被另一个线程
                    *       中断阻塞状态时,当前线程会抛出该异常。
                    * */
                    //阻塞当前主线程
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    /**
     * 中断异常演示:仿装修小品
     */
    public class InterruptedExceptionTest {
        public static void main(String[] args){
            /*
            * 林永健进入睡眠状态
            *
            * 方法中定义的类叫做局部内部类
            * 局部内部类中若想引用当前方法的其它局部变量
            * (参数也是方法的局部变量),
            * 那么该变量必须是final的。
            * */
            final Thread lin = new Thread(){
                @Override
                public void run() {
                    System.out.println("林:睡觉了。。。");
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        System.out.println("林:干嘛呢!干嘛呢!干嘛呢!");
                        System.out.println("林:都破了相了");
                    }
                }
            };
            lin.start();//启动第一个线程
            Thread huang = new Thread(){
                @Override
                public void run() {
                    System.out.println("黄:80一锤子,您说砸哪儿?");
                    for (int i = 0; i < 5; i++) {
                        System.out.println("黄:80!");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("咣当!");
                    System.out.println("黄:搞定!");
                    lin.interrupt();//中断第一个线程的阻塞状态
                }
            };
            huang.start();//启动第一个线程
        }
    }
    

    线程其它方法

    /**
     * 线程的方法:
     * yield():放弃当次时间片,主动进入Runnable状态
     * setPriority():设置线程优先级
     * 优先级越高的线程,理论上获取cpu的次数就越多
     * 设置线程优先级一定要在线程启动前设置!
     */
    public class ThreadOtherMethod {
        public static void main(String[] args){
            Thread t1 = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("你是谁啊?");
                        Thread.yield();
                    }
                }
            };
            Thread t2 = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("我是修水管的。");
                        Thread.yield();
                    }
                }
            };
            Thread t3 = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("我是打酱油的");
                        Thread.yield();
                    }
                }
            };
            t1.setPriority(Thread.MAX_PRIORITY);
            t2.setPriority(Thread.MIN_PRIORITY);
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    线程并发安全问题

    多线程在访问同一个数据时(写操作),可能会引发不安全操作。

    • volatile关键字:用volatile修饰的变量,线程在每次读取变量的时候,都会从主内存中读取最新的值;每次给变量赋值时,都会把线程工作区变量的值同步到主内存。volatile用来进行原子性操作。当变量n=n+1、n++ 等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,volatile才有效。
    /**
     * 多线程并发安全问题
     *
     * synchronized关键字
     * 线程安全锁
     * synchronized可以修饰方法也可以单独作为语句块存在
     * synchronized的作用是限制多线程并发同时访问该作用域
     */
    public class ThreadSecure {
        public static void main(String[] args){
            /*main这个静态方法中只能访问静态内部类,所有Bank要用static修饰,
            * 不用static修饰的话会提示
            * 'com.example.ThreadSecure.this' cannot be referenced from a static context错误
            * */
            /*
            * 创建银行
            * 创建两个Person线程实例,并发从当前银行对象中访问数据
            * */
            Bank bank = new Bank();
            /*下面这样创建会提示
            'com.example.ThreadSecure.Bank' is not an enclosing class错误
            没有静态(static)修饰的类中类不能使用外部类进行.操作,
            必须用实例来进行实例化类中类。
            */
            //new Bank.Person();
            //非静态内部类的创建和调用类实例的方法类似,类实例.new 非静态内部类名();
            Bank.Person p1 = bank.new Person();
            Bank.Person p2 = bank.new Person();
            p1.start();
            p2.start();
        }
        static class Bank{
            int count = 10000;
            //取钱方法
            /*
            * synchronized修饰方法后,方法就不是异步的了,而是同步的了,
            * synchronized会为方法上锁。
            *
            * synchronized同步块
            * synchronized (Object)
            * {需要同步的代码块片段}
            * Object指的是要上锁的对象,
            * 这里必须注意!要确保所有线程看到的是同一个对象!否则起不到同步的效果!
            * */
            synchronized void getMoney(int money){
                synchronized (this){
                    if (count==0){
                        throw new RuntimeException("余额为0");
                    }
                /*try {
                    Thread.sleep(60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                    Thread.yield();
                    count-=money;
                    System.out.println("当前余额"+count);
                }
            }
            /*线程里面代码出错且未捕获异常,当前线程会被杀死,
            但程序还是继续运行,程序不会退出*/
            class Person extends Thread{
                @Override
                public void run() {
                    while (true){
                        getMoney(100);
                    }
                }
            }
        }
    }
    

    后台线程

    /**
     * 后台线程也称为守护线程
     * 特点:
     *      当当前进程中所有前台线程死亡后,后台线程强制死亡。无论
     *      是否还在运行。
     */
    
    public class DaemonThread {
        public static void main(String[] args){
            Thread rose = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("let me go!");
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("AAAAaaaaaaa......噗通!");
                }
            };
            Thread jack = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("you jump!i jump!");
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            jack.setDaemon(true);//设置守护线程,必须在线程启动之前设置
            rose.start();
            /*在执行完成main方法的主线程执行完毕死亡后,
            接着rose线程执行完毕相继死亡,这时前台线程都死亡了,
            守护线程jack强制死亡*/
            jack.start();
        }
    }
    
    • wait/notify方法
      这两个方法不是线程Thread中定义的方法,这两个方法定义在Object中。这两个方法的作用是用于协调线程工作的。wait/notify方法必须保证在synchronized块里面,而且等待哪个对象上就在哪个对象上上锁,通知等待的线程运行和通知synchronized块上的对象就是等待时上锁的那个对象。
    public class ThreadCoordinate {
        public static void main(String[] args){
            /*
            * 创建银行
            * 创建两个Person线程实例,并发从当前银行对象中访问数据
            * */
            Bank bank = new Bank();
            Bank.Person p1 = bank.new Person();
            Bank.Person p2 = bank.new Person();
            p1.start();
            p2.start();
            /*p1和p2都在bank对象上等待了。进入了阻塞*/
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            bank.count=10000;
            System.out.println("银行开门了");
            //bank初始化完毕了
            synchronized (bank){
                bank.notifyAll();//通知在银行上等待的所有线程可以工作了
            }
        }
        static class Bank{
            //count这个私有变量为什么可以在ThreadCoordinate这个类的main方法中访问?
            /*内部类就相当于一个外部类的成员变量,所以可以直接访问外部变量,
            外部类不能直接访问内部类变量,必须通过创建内部类实例的方法访问。
            想不通的肯定是指内部类的私有变量怎么可以被外部类访问吧,
            按常规,私有变量m只能在InnerClass里被访问,
            但你要注意,内部类就相当于一个外部类的成员变量,举个例子。
            class Outer{
                private int m;
                private class Inner{
                    private int n;
                    private int k;
                }
            }
            m和类Inner都是成员变量,他们之间是平等的,唯一不同的
            就是Inner它是包装了几个成员变量比如n,k,也就是说m n k是平等的,
            区别在于访问n k要通过Inner,就是要建立Inner实例访问n k
            */
            private int count;
            //取钱方法
            void getMoney(int money){
                synchronized (this){
                    if (count==0){
                        throw new RuntimeException("余额为0");
                    }
                    Thread.yield();
                    count-=money;
                    System.out.println("当前余额"+count);
                }
            }
            class Person extends Thread{
                @Override
                public void run() {
                    System.out.println("准备取钱,等待银行开门!");
                    synchronized (Bank.this){
                        try {
                            Bank.this.wait();//当前线程(Person)在银行对象上等
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    while (true){
                        getMoney(100);
                    }
                }
            }
        }
    }
    

    线程池

    • 线程若想启动需要调用start()方法。这个方法要做很多操作。要和操作系统打交道,注册线程等工作,等待线程调度。
    • ExecutorService提供了管理终止线程池的方法。
    • 线程池的创建都是工厂方法。我们不要直接去new线程池,因为线程池的创建还要做很多的准备工作。
    • Executors.newCachedThreadPool();
      可根据任务需要动态创建线程,来执行任务。若线程池中有空闲的线程将重用该线程来执行任务。没有空闲的则创建新线程来完成任务。理论上池子里可以放int最大值个线程。线程空闲时长超过1分钟将会被回收。一般用于处理执行时间比较短的任务。(第二常用
    • Executors.newFixedThreadPool();创建固定大小的线程池。池中的线程是固定的。若所有线程处于饱和状态,新任务将排队等待。(最常用
    • Executors.newScheduledThreadPool();
      创建具有延迟效果的线程池。可将待运行的任务延迟指定时长后再运行。
    • Executors.newSingleThreadExecutor();
      创建单线程的线程池。池中仅有一个线程。所有未运行的任务排队等待。
    import java.io.BufferedReader;
    import java.io.FileWriter;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.Timer;
    import java.util.TimerTask;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 服务端
     * 加入双缓冲队列,加快读写数据操作。
     * 双缓冲队列可以规定队列存储元素的大小。
     * 一旦队列中的元素达到最大值,待插入的元素将等待。
     * 等待时间是给定的。当给定时间到了元素还没有机会被放入队列
     * 那么会抛出超时异常。
     */
    
    public class ServerDemo {
        public static void main(String[] args){
            System.out.println("服务器启动中......");
            ServerDemo demo = new ServerDemo();
            demo.start();//连接服务器并通信
        }
        private ServerSocket socket;
        private int port = 8088;
        //线程池
        private ExecutorService threadPool;
        /*构建ServerDemo对象时就打开服务器端口*/
        public ServerDemo(){
            try {
                /*
                * ServerSocket构造对象要求我们传入要打开的端口号
                * ServerSocket对象在创建的时候就向操作系统申请打开
                * 这个端口。
                * */
                socket = new ServerSocket(port);
                //创建50个线程的固定大小的线程池
                threadPool = Executors.newFixedThreadPool(50);   
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        /*
        * 开始服务
        * 等待接收客户端的请求并与其通信
        * */
        public void start(){
            try {
                /*通过调用ServerSocket的accept方法,使服务器开始等待
                * 接收客户端的连接。
                * 该方法是一个阻塞方法,监听8088端口是否有客户端连接。
                * 直到有客户端与其连接,否则该方法不会结束。
                * */
                while (true){
                    System.out.println("等待客户端连接......");
                    Socket s = socket.accept();
                    System.out.println("一个客户端连接了,分配线程去接待它");
                    /*当一个客户端连接了,就启动一个线程去接待它*/
                    //Thread clientThread = new Thread(new Handler(s));
                    //clientThread.start();
                    /*
                    * 将线程体(并发的任务)交给线程池
                    * 线程池会自动将该任务分配给一个空闲线程去执行。
                    * */
                    threadPool.execute(new Handler(s));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        /*定义线程体。该线程的作用是与连接到服务器端的客户端
        进行交互操作*/
        class Handler implements Runnable{
            //当前线程要进行通信的客户端Socket
            private Socket socket;
            //通过构造方法将客户端的Socket传入
            public Handler(Socket socket){
                this.socket = socket;
            }
            @Override
            public void run() {
                try {
                    InputStream in = socket.getInputStream();
                    OutputStream out = socket.getOutputStream();
                    PrintWriter writer = new PrintWriter(out);
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    //先听客户端发送的信息
                    String info = reader.readLine();//这里同样会阻塞
                    System.out.println(info);
                    //发送信息给客户端
                    writer.println("你好!客户端");
                    writer.flush();
                    info = reader.readLine();
                    System.out.println(info);
                    writer.println("再见!客户端");
                    writer.flush();
                    socket.close();//关闭与客户端的连接
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.Socket;
    
    /**
     * MINA框架 开源的socket框架。很多游戏服务器的通信框架全用它
     *
     * 客户端
     */
    
    public class ClientDemo {
        public static void main(String[] args){
            ClientDemo demo = new ClientDemo();
            demo.send();//连接服务器并通信
        }
        private Socket socket;
        /*
        * 建立连接并向服务器发送信息
        * 步骤:
        *       1:通过服务器的地址及端口与服务器连接
        *       创建Socket时需要以上两个数据
        *       2:连接成功后可以通过Socket获取输入流和输出流
        *       使用输入流接收服务端发送过来的信息
        *       使用输出流信息发送给服务端
        *       3:关闭连接
        * */
        public void send(){
            try {
                System.out.println("开始连接服务器");
                /*1、连接服务器
                * 一旦Socket被实例化,那么它就开始通过给定的地址和
                * 端口号去尝试与服务器进行连接。
                * 这里的地址“localhost”是服务器的地址
                * 8088端口是服务器对外的端口。
                * 我们自身的端口是系统分配的,我们无需知道。
                * */
                socket = new Socket("localhost",8088);
                /**
                 * 和服务器通信(读写数据)
                 * 使用socket获取输入和输出流
                 */
                InputStream in = socket.getInputStream();
                OutputStream out = socket.getOutputStream();
                /*将输出流变成处理字符串的缓冲字符输出流*/
                PrintWriter writer = new PrintWriter(out);
                writer.println("你好!服务器!");
                /*
                * 注意,写到输出流的缓冲区里了,并没有真的发给服务器
                * 想真的发送就要做真实的写操作,清空缓冲区
                * */
                writer.flush();
                //将输入流转换为缓冲字符输入流
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(in)
                );
                /*读取服务器发送过来的信息*/
                String info = reader.readLine();//读取服务器信息会阻塞
                System.out.println(info);
                writer.println("再见!服务器!");
                writer.flush();
                info = reader.readLine();
                System.out.println(info);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
    • 双缓冲队列:BlockingQeque:解决了读写数据阻塞问题。但是同时写或读还是同步的。
       //双缓冲队列
        private BlockingQueue<String> msgQueue;
    
                /*
                * 创建规定大小的双缓冲队列
                * LinkedBlockingQueue是一个可以不指定队列大小的
                * 双缓冲队列。若指定大小,当达到峰值后,待入队的将
                * 等待。理论上最大值为int最大值
                * */
                msgQueue = new LinkedBlockingQueue<String>(10000);
                /*
                * 创建定时器,周期性的将队列中的数据写入文件
                * */
                Timer timer =new Timer();
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        try {
                            PrintWriter writer =
                                    new PrintWriter(new FileWriter("log.txt",true));
                            //从队列中获取所有元素,做写出操作
                            String msg = null;
                            for (int i = 0; i < msgQueue.size(); i++) {
                                /*
                                * 参数 0:时间量
                                *         TimeUnit.MILLISECONDS:时间单位
                                * */
                                msg = msgQueue.poll(0, TimeUnit.MILLISECONDS);
                                if (msg==null){
                                    break;
                                }
                                writer.println(msg);//通过输出流写出数据
                            }
                            writer.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                },0,500);
    
                    String msg = null;
                    while (true){
                        //循环读取客户端发送过来的信息
                        msg = reader.readLine();
                        if (info!=null){
                            //插入队列成功返回true,失败返回false。
                            //该方法会阻塞线程,若中断会报错!
                            boolean b = msgQueue.offer(msg,5,TimeUnit.SECONDS);
                        }
                    }
    

    相关文章

      网友评论

          本文标题:多线程

          本文链接:https://www.haomeiwen.com/subject/fnodpftx.html