多线程基本概念理解

初识多线程

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(LightWeight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。在使用迅雷下载电影时,迅雷.exe可以看作一个进程,而同时在下载着A,B,C三个电影,就可以看作3个线程。

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class MainThread {

    public static void main(String[] args) {
        // 获取Java线程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        }

    }
}
[5] Attach Listener
[4] Signal Dispatcher
[3] Finalizer
[2] Reference Handler
[1] main

可以看到,一个Java程序的运行不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行。

使用多线程的两种方法

继承Thread类

线程运行结果与代码执行顺序无关

public class ThreadHandler {

    public static void main(String[] args) {
        Thread thread = new MyThread();
        System.out.println("begin");
        thread.start();
        System.out.println("end");
    }

}

class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println(Thread.currentThread().getName());
    }
}
begin
end
Thread-0

从上面程序运行结果看,线程运行结果与代码执行顺序无关。

执行start的顺序不代表线程启动的顺序

public class ThreadHandler {

    private static int threadNum = 5;

    public static void main(String[] args) {
        System.out.println("begin");
        Thread[] threads = new Thread[threadNum];
        for (int i = 0; i < threadNum; i++) {
            threads[i] = new MyThread(" index" + i);
            threads[i].start();
        }
        System.out.println("end");
    }

}

class MyThread extends Thread {
    private String index;

    public MyThread(String index) {
        this.index = index;
    }

    @Override
    public void run() {
        super.run();
        System.out.println("ThreadName=" + Thread.currentThread().getName() + index);
    }
}
begin
ThreadName=Thread-0 index0
end
ThreadName=Thread-2 index2
ThreadName=Thread-4 index4
ThreadName=Thread-3 index3
ThreadName=Thread-1 index1

从上面程序运行结果看,执行start的顺序也不代表线程启动的顺序。

Thread类中的start方法通知“线程规划器”此线程已经准备就绪,等待调用线程的run方法。这个过程其实是让系统啊弄一个时间来调用run方法,也就是使线程运行,启动线程具有异步执行效果。

调用start与调用run的区别

public class ThreadHandler {

    private static int threadNum = 5;

    public static void main(String[] args) {
        System.out.println("begin");
        Thread[] threads = new Thread[threadNum];
        for (int i = 0; i < threadNum; i++) {
            threads[i] = new MyThread(" index" + i);
            threads[i].run();
        }
        System.out.println("end");
    }

}

class MyThread extends Thread {
    private String index;

    public MyThread(String index) {
        this.index = index;
    }

    @Override
    public void run() {
        super.run();
        System.out.println("ThreadName=" + Thread.currentThread().getName() + index);
    }
}
begin
ThreadName=main index0
ThreadName=main index1
ThreadName=main index2
ThreadName=main index3
ThreadName=main index4
end

观察执行结果发现,线程名都是main,并且index是按顺序执行的。因此,main函数中调用run与调用普通的成员函数没有一样,并不会启动新的线程。

实现runnable接口

public class RunableHandler {

    public static void main(String[] args) {
        Runnable runner = new Runner();
        Thread thread = new Thread(runner);
        thread.start();

    }

}

class Runner implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());

    }
}
Thread-0

从上面程序可以看出,实现了runnable接口,不能直接调用start方法,而需要通过runnable对象构造Thread类,实现线程的启动。

   public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
   public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }

Thread的其中两个构造函数可以看出,他们都支出传入runnable对象。

继承Thread类与实现Runnable接口的区别

java不支持多继承,使用继承Thread类的实现多线程将不能继承其它任何类。而实现Runnable接口的方法没有这个限制。

线程的安全与使用

先看《java并发编程实战》》中的一个定义:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用代码代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

一个线程不安全例子

我们先看一个线程不安全例子,有一个典型的售票场景:5个售票员出售10张票,并且不能不同售票员出售同一张票,直到票出售完为止。

public class Counter {

    private static int tickets = 10;
    private static int threadNum = 5;

    public static void main(String[] args) {
        Runnable counter = new CounterRunner(tickets);
        Thread[] threads = new Thread[threadNum];

        for (int i = 0; i < threadNum; i++) {
            threads[i] = new Thread(counter, "售票员" + i);

        }
        for (Thread thread : threads) {
            thread.start();
        }
    }
}

class CounterRunner implements Runnable {
    private int tickets;

    public CounterRunner(int tickets) {
        this.tickets = tickets;
    }

    @Override
    public void run() {
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " 出售票号" + tickets--);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
售票员0 出售票号10
售票员1 出售票号9
售票员4 出售票号8
售票员2 出售票号7
售票员3 出售票号6
售票员1 出售票号5
售票员4 出售票号5
售票员0 出售票号4
售票员3 出售票号2
售票员2 出售票号3
售票员1 出售票号1
售票员0 出售票号0
售票员4 出售票号0

程序运行可以看出售票员1和4都出售了票号5,与期望不一致,CounterRunner类是线程不安全的。

为什么回出现这种情况呢?
这个例子中,售票员1和售票员4两个线程同时对CounterRunner对象的tickets进行了处理, 当售票员1取得tickets的票号5还没来得急做-1的操作,售票员4也取得了票号5,对票5进行了重复的处理,引起线程不安全。在JVM中,i–分为3步:
1. 取得原来的i值
2. 计算i-1
3. 对i重新赋值

数据共享下的线程安全

因此我们可以得到初步结论,编写正确的并发程序关键在于对共享变量的访问管理,要确保同一时间只有一个线程对共享数据进行访问。下面简单改一下原来代码,使用synchronized确保共享数据同步,保证线程的安全性。

public class Counter {

    private static int tickets = 10;
    private static int threadNum = 5;

    public static void main(String[] args) {
        Runnable counter = new CounterRunner(tickets);
        Thread[] threads = new Thread[threadNum];

        for (int i = 0; i < threadNum; i++) {
            threads[i] = new Thread(counter, "售票员" + i);

        }
        for (Thread thread : threads) {
            thread.start();
        }
    }
}

class CounterRunner implements Runnable {
    private int tickets;

    public CounterRunner(int tickets) {
        this.tickets = tickets;
    }

    @Override
    public void run() {
        while (tickets > 0) {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + " 出售票号" + tickets--);
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
售票员0 出售票号10
售票员4 出售票号9
售票员1 出售票号8
售票员3 出售票号7
售票员2 出售票号6
售票员4 出售票号5
售票员2 出售票号4
售票员0 出售票号3
售票员1 出售票号2
售票员3 出售票号1

共享 数据情况,大多是多个线程可以访问同一个变量,比如买票,投票等,可以抽象出模型如下图。
共享数据情况

数据不共享下的线程安全

还是买票的例子,前面出现线程安全是由于多个线程同时对tickets变量同时操作。现在我们换个思路,先将所有票号按线程数分好,然后交给每个线程处理,各线程拥有各自的变量,自己减少自己变量的值。这种情况下不存在数据共享,不会多个线程访问同一个变量。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class Counter {
    private static int ticketNum = 10;
    private static int threadNum = 5;

    public static void main(String[] args) {

        Map<Integer, List<String>> ticketsMap = new HashMap<Integer, List<String>>();
        //为每个线程切分变量
        for (int i = 0; i < ticketNum; i++) {
            List<String> tickets = ticketsMap.get(i % threadNum);
            if (tickets == null) {
                tickets = new ArrayList<String>();
            }
            tickets.add("票" + i);
            ticketsMap.put(i % threadNum, tickets);
        }

        for (Entry<Integer, List<String>> entry : ticketsMap.entrySet()) {
        //每个CounterThread对象都有独立的tickets
            new Thread(new CounterThread(entry.getValue()), "售票处" + entry.getKey()).start();
        }

    }

}

class CounterThread extends Thread {
    private List<String> tickets;

    public CounterThread(List<String> tickets) {
        this.tickets = tickets;
    }

    @Override
    public void run() {
        for (String ticket : tickets) {
            System.out.println(Thread.currentThread().getName() + " 出售了" + ticket);
        }
        System.out.println(Thread.currentThread().getName() + " 没票了");
    }
}
售票处0 出售了票0
售票处2 出售了票2
售票处1 出售了票1
售票处2 出售了票7
售票处0 出售了票5
售票处2 没票了
售票处1 出售了票6
售票处3 出售了票3
售票处3 出售了票8
售票处0 没票了
售票处3 没票了
售票处1 没票了
售票处4 出售了票4
售票处4 出售了票9
售票处4 没票了

数据不共享的情况实现多线程,可以使用如下图的模型。
这里写图片描述

为什么要使用多线程

  • 更多的处理器核心
  • 更快的响应时间
  • 更好的编程模型

更多的处理器核心

随着处理器上的核心数量越来越多,以及超线程技术的广泛运用,现在大多数计算机都比以往更加擅长并行计算,而处理器性能的提升方式,也从更高的主频向更多的核心发展。如何利用好处理器上的多个核心也成了现在的主要问题。
因为线程调度的基本单元是线程,一个单线程应用程序一次只能运行在一个处理器上。在双处理器系统中,一个单线程程序,放弃了其中一半的空闲资源;在拥有100个处理器系统中,一个单线程程序,放弃了一半的99%资源;从另一方面来看,拥有多个活跃线程的程序可以同时在多个处理器上运行。在设计良好的情况下,多线程程序通过更有效的利用处理器资源,来提供吞吐量。(等待水烧开时间里看报纸,优于等待开水开之后再去看报纸)

还是买票的例子,5个售票员10张票,每买一张耗时1s。理论上使用当只有1个售票员就需要10s,下面看下多线程情况下的耗时:

数据共享方式实现

package objective1.action3;

import java.util.concurrent.CountDownLatch;

public class Counter {

    private static int tickets = 10;
    private static int threadNum = 5;
    private static CountDownLatch timeLatch = new CountDownLatch(threadNum);

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        Runnable counter = new CounterRunner(tickets, timeLatch);
        Thread[] threads = new Thread[threadNum];

        for (int i = 0; i < threadNum; i++) {
            threads[i] = new Thread(counter, "售票员" + i);

        }
        for (Thread thread : threads) {
            thread.start();
        }

        try {
            timeLatch.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        long takeTime = System.currentTimeMillis() - startTime;
        System.out.println("takeTime=" + takeTime + "ms");
    }

}

class CounterRunner implements Runnable {
    private int tickets;
    private CountDownLatch timeLatch;

    public CounterRunner(int tickets, CountDownLatch timeLatch) {
        this.tickets = tickets;
        this.timeLatch = timeLatch;
    }

    @Override
    public void run() {
        while (tickets > -1) {
            synchronized (this) {
                if (tickets == 0) {
                    System.out.println(Thread.currentThread().getName() + " 没票了");
                    timeLatch.countDown();
                    return;
                } else {
                    System.out.println(Thread.currentThread().getName() + " 出售票号" + tickets--);
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
售票员0 出售票号10
售票员2 出售票号9
售票员1 出售票号8
售票员3 出售票号7
售票员4 出售票号6
售票员2 出售票号5
售票员0 出售票号4
售票员1 出售票号3
售票员3 出售票号2
售票员4 出售票号1
售票员1 没票了
售票员0 没票了
售票员2 没票了
售票员4 没票了
售票员3 没票了
takeTime=2004ms

数据不共享方式实现

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class Counter_1_14 {
    private static int ticketNum = 10;
    private static int threadNum = 5;
    private static CountDownLatch timeLatch = new CountDownLatch(threadNum);

    public static void main(String[] args) {

        long startTime = System.currentTimeMillis();
        List<List<String>> ticketsList = initTicketsArray();
        for (int i = 0; i < ticketsList.size(); i++) {
            new CounterThread1(ticketsList.get(i), "售票处" + i, timeLatch).start();
        }

        try {
            timeLatch.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        long takeTime = System.currentTimeMillis() - startTime;
        System.out.println("耗时=" + takeTime);
    }

    private static List<List<String>> initTicketsArray() {
        List<List<String>> ticketsList = new ArrayList<List<String>>();
        List<String> tempList = null;
        int index = 0;
        for (int i = 0; i < ticketNum; i++) {
            if (i < threadNum) {
                tempList = new ArrayList<String>();
                ticketsList.add(tempList);
            }
            ticketsList.get(index++).add("票" + i);
            if (index >= threadNum) {
                index = 0;
            }
        }
        return ticketsList;
    }

}

class CounterThread1 extends Thread {
    private List<String> tickets;
    private String name;
    private CountDownLatch timeLatch;

    public CounterThread1(List<String> tickets, String name, CountDownLatch timeLatch) {
        this.tickets = tickets;
        this.name = name;
        this.timeLatch = timeLatch;
    }

    @Override
    public void run() {
        for (String ticket : tickets) {
            System.out.println(name + " 出售了" + ticket);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println(name + " 没票了");
        timeLatch.countDown();
    }
}
售票处1 出售了票1
售票处3 出售了票3
售票处2 出售了票2
售票处0 出售了票0
售票处4 出售了票4
售票处4 出售了票9
售票处1 出售了票6
售票处0 出售了票5
售票处3 出售了票8
售票处2 出售了票7
售票处4 没票了
售票处1 没票了
售票处0 没票了
售票处3 没票了
售票处2 没票了
耗时=2009

更好的编程模型

Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型,而不是绞尽脑汁地考虑如何将其多线程化。一旦开发人员建立好了模型,稍做修改总是能够方便地映射到Java提供的多线程编程模型上。

生产者-消费者模式

该模式将“找出需要完成的工作”与“执行工作”者两个过程分离开,并把工作项放入一个待完成列表中以便在随后处理,而不是立即处理。生产者-消费者模式能简化开发过程,因为它消除了生产者和消费者之间的代码依赖,此外,该模式还将生产数据的过程与使用数据的过程解耦开来以简化工作的负荷管理,因为这两个过程在处理数据的速率上有所不同。
以买票例子为例,出票和买票也是一种生产者-消费者模式:其中一部分人把票准备好,分到各个售票处,而另一部分人从各个售票处把票买走。这个例子中,售票处相当于一个阻塞队列。如果售票处没有票,那么消费者需要一直等待,直到票被生产好送到售票处。这样每一部分人只需要与售票处大家的,人们不需要直到他有多少生产者或消费者,或者这个票是谁生产的,票将卖给谁。

package objective1.action3;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class DispatchManger {

    private static int tickesNum = 10;

    public static void main(String[] args) {
        List<Ticket> tickets = new ArrayList<Ticket>();
        for (int i = 0; i < tickesNum; i++) {
            tickets.add(new Ticket("票号" + i, i));
        }
        TicktsDispatcher.getInstance().dispatch(tickets);
    }

}

class TicktsDispatcher {

    private static int threadNum = 5;
    private static TicktsDispatcher instance = new TicktsDispatcher();

    public static TicktsDispatcher getInstance() {
        return instance;
    }

    TicketConsumer[] ticketConsumers = new TicketConsumer[threadNum];

    public TicktsDispatcher() {
        for (int i = 0; i < threadNum; i++) {
            ticketConsumers[i] = new TicketConsumer();
        }

        for (int i = 0; i < threadNum; i++) {
            new Thread(ticketConsumers[i], "售票处" + i).start();
        }
    }

    public void dispatch(List<Ticket> tickets) {
        for (Ticket ticket : tickets) {
            int index = ticket.getIndex() % threadNum;
            ticketConsumers[index].add(ticket);
        }
    }

}

class TicketConsumer implements Runnable {
    private BlockingQueue<Ticket> ticketQuery = new LinkedBlockingQueue<>();

    @Override
    public void run() {
        while (true) {
            try {
                Ticket ticket = ticketQuery.take();
                ticketProcess(ticket);

            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    public void add(Ticket ticket) {
        try {
            ticketQuery.put(ticket);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private void ticketProcess(Ticket ticket) {
        System.out.println(Thread.currentThread().getName() + "出售了" + ticket.getTicketName());

    }

}

class Ticket {
    private String ticketName;
    private int index;

    public Ticket(String ticketName, int index) {
        this.ticketName = ticketName;
        this.index = index;
    }

    public String getTicketName() {
        return ticketName;
    }

    public int getIndex() {
        return index;
    }

}
售票处0出售了票号0
售票处1出售了票号1
售票处4出售了票号4
售票处3出售了票号3
售票处2出售了票号2
售票处3出售了票号8
售票处4出售了票号9
售票处1出售了票号6
售票处0出售了票号5
售票处2出售了票号7

ref

JAVA并发编程实践
Java并发编程的艺术
Java多线程编程核心技术
http://ifeve.com/tag/concurrency/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值