Java 多线程
多线程比较典型的应用,如:① 迅雷、FlashGet 多线程下载;② 创建灵活响应的桌面程序。
单核CPU来说,线程在宏观上是并行的,在微观上是串行的。
概念
进程
- 运行时(runtime)应用程序。
- 进程之间的内存不是共享(独占)
- 进程间通信使用的socket(套接字)
线程
- 进程内并发执行的代码段
- 线程之间共享内存
- 可以创建灵活响应的桌面程序
- 每个运行着的线程对应一个stack
- 应用程序至少有一个线程(主线程)
创建多线程的方法
继承Thread类
- 子类覆盖父类中的run方法,将线程运行的代码存放在run中。
- 建立子类对象的同时线程也被创建
- 通过调用start方法开启线程。
用start启动线程而不是run(),原因可以看java源码里,通过native关键字调用本地start0线程启动线程。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
一个实现案例
ThreadDemo1.java
class ThreadDemo1{
public static void main(String[] args){
//创建线程对象
MyThread t1 = new MyThread();
YourThread t2 = new YourThread();
//
t1.start();
t2.start();
}
}
//线程1
class MyThread extends Thread{
public void run(){
for(;;){
System.out.println("hello world - 1");
}
}
}
class YourThread extends Thread{
public void run(){
for(;;){
System.out.println("hello world - 2");
}
}
}
线程创建后,线程t1和t2各自有一块栈空间,栈中压入的是run()方法。
main是主线程,压入的是main方法。
实现Runnable接口
Thread 其实也是实现了Runnable接口,线程的启动相当于代理模式。
实现Callable接口
线程启动还可以实现Callable接口。
java.lang.Thread中的API方法学习
yield 方法
让当前线程让出cpu抢占权,具有谦让之意,瞬时的动作。
ThreadDemo2.java
① 当没有添加yield方法时
class ThreadDemo2{
public static void main(String[] args){
MyThread t1 = new MyThread("Thread-1");
MyThread t2 = new MyThread("Thread-2");
t1.start();
t2.start();
}
}
class MyThread extends Thread{
private String name;
public MyThread(){}
public MyThread(String name){
this.name = name;
}
@Override
public void run(){
while(true){
System.out.println(name);
//线程释放资源,处于Runnable的状态
//Thread.yield();
//加上yield后的直观体会是线程间的
}
}
}
输出结果看,Thread-1 和 Thread-2 交替很慢
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
① 当没有添加yield方法时
class ThreadDemo2{
public static void main(String[] args){
MyThread t1 = new MyThread("Thread-1");
MyThread t2 = new MyThread("Thread-2");
t1.start();
t2.start();
}
}
class MyThread extends Thread{
private String name;
public MyThread(){}
public MyThread(String name){
this.name = name;
}
@Override
public void run(){
while(true){
System.out.println(name);
//线程释放资源,处于Runnable的状态
Thread.yield();
//加上yield后的直观体会是线程间的
}
}
}
输出结果看,Thread-1 和 Thread-2 交替加快了。yield在线程中有”谦让”之意,英语之中是“屈服”的意思。
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-1
Thread-2
Thread-2
Thread-2
Thread-2
Thread-1
Thread-1
Thread-1
Thread-1
Thread-2
Thread-2
Thread-1
Thread-1
Thread-1
Thread-2
Thread-1
Thread-1
sleep 方法
让当前线程休眠指定毫秒数,释放cpu抢占权,和锁旗标的监控权没有关系。
join
当前线程等待指定的线程结束后才能继续运行。
以打麻将为例子。现在一个老板要找四个人打麻将,等待四个人赶过来后,开局。
ThreadDemo3.java
class ThreadDemo3{
public static void main(String[] args){
Player p1 = new Player("施瓦辛格",3000);
Player p2 = new Player("成 龙",1000);
Player p3 = new Player("李 连 杰",2000);
Player p4 = new Player("史 泰 龙",2500);
//
p1.start();
p2.start();
p3.start();
p4.start();
try{
p1.join();
p2.join();
p3.join();
p4.join();
}
catch(Exception e){
}
//
System.out.println("开局!");
}
}
//线程1
class Player extends Thread{
private String name ;
private int time ;
public Player(String name,int time){
this.name = name ;
this.time = time ;
}
public void run(){
System.out.println(name + "出发了!");
try{
//
Thread.sleep(time);
}
catch(Exception e){
}
System.out.println(name + "到场了.===>耗时 : " + time);
}
}
daemon 方法
守护线程,类似“服务员”。最典型的是:java的垃圾回收。
Thread.setDaemon(true);
ThreadDemo4.java
class ThreadDemo4{
public static void main(String[] args){
Room r1 = new Room("001",1000);
Room r2 = new Room("002",2000);
Room r3 = new Room("003",3000);
Waiter w = new Waiter();
//设置线程为守护线程
w.setDaemon(true);
r1.start();
r2.start();
r3.start();
try{
r1.join();
r2.join();
r3.join();
}catch(InterruptedException ie){
ie.printStackTrace();
}
System.out.println("\n结束营业");
}
}
class Room extends Thread{
private String no;
private int time;
public Room(){}
public Room(String no,int time){
this.no = no;
this.time = time;
}
@Override
public void run(){
System.out.println(no+ " 包房开始消费...");
try{
Thread.sleep(time);
}catch(InterruptedException ie){
ie.printStackTrace();
}
System.out.println(no+ " 包房消费了"+time+"秒");
}
}
class Waiter extends Thread
{
@Override
public void run(){
while (true){
System.out.println("服务员服务中...."+new java.util.Date());
}
}
}
输出
No2号包房开始消费!
No1号包房开始消费!
Wed Jul 20 14:43:24 CST 2016
Wed Jul 20 14:43:25 CST 2016
Wed Jul 20 14:43:25 CST 2016
Wed Jul 20 14:43:26 CST 2016
Wed Jul 20 14:43:26 CST 2016
Wed Jul 20 14:43:27 CST 2016
No1号包房消费时间 : 3000,结束消费!
Wed Jul 20 14:43:27 CST 2016
Wed Jul 20 14:43:28 CST 2016
Wed Jul 20 14:43:28 CST 2016
Wed Jul 20 14:43:29 CST 2016
Wed Jul 20 14:43:29 CST 2016
Wed Jul 20 14:43:30 CST 2016
Wed Jul 20 14:43:30 CST 2016
Wed Jul 20 14:43:31 CST 2016
No2号包房消费时间 : 7000,结束消费!
从输出中,我们也可以看到,当被守护线程结束,守护线程才结束。
线程间通信
先从一个案例看起,领会线程间通信的必要性。
以2售票个售票员共同售100张票为例。
ThreadDemo5.java
class ThreadDemo5{
public static void main(String[] args){
Saler s1 = new Saler("Saler1");
Saler s2 = new Saler("Saler2");
s1.start();
s2.start();
}
}
class Saler extends Thread{
private String name;
private static int tickets = 100;
public Saler(){}
public Saler(String name){
this.name = name;
}
@Override
public void run(){
while(tickets>0){
System.out.println(name+ ": " + tickets--);
}
}
}
可能出现的错误情形
Saler1: 100
Saler2: 100
Saler1: 99
Saler1: 97
Saler1: 96
Saler1: 95
Saler2: 98
Saler2: 93
Saler1: 94
Saler2: 92
Saler1: 91
……
有时会买出重复的票,这就造成数据的不一致。
同步代码块
线程间共享资源,并发访问,为了确保数据的一致性,解决的方法是加锁。使得线程访问共享资源由并行转为串行。
线程间通信,需要一个“参照物”,锁旗标,类似“信号灯”。锁标记需要线程有“锁定权”。
修改后
ThreadDemo5.java
class ThreadDemo5{
public static void main(String[] args){
Saler s1 = new Saler("S1");
Saler s2 = new Saler("S2");
s1.start();
s2.start();
}
}
//售票员
class Saler extends Thread{
static int tickets = 100 ;
//锁旗标
static Object lock = new Object();
private String name ;
public Saler(String name){
this.name = name ;
}
public void run(){
while(true){
int t = getTicket();
if(t == -1){
return ;
}
else{
System.out.println(name + " : " + t);
}
}
}
//取票
public int getTicket(){
synchronized(lock){
int t = tickets ;
try{
Thread.sleep(500);
}
catch(Exception e){
}
tickets = tickets - 1 ;
return t < 1 ? -1 : t ;
}
}
}
输出:
S1 : 100
S2 : 99
S1 : 98
S2 : 97
S1 : 96
S2 : 95
S1 : 94
S2 : 93
S1 : 92
S2 : 91
S1 : 90
S2 : 89
S1 : 88
S2 : 87
S1 : 86
S2 : 85
S1 : 84
S2 : 83
S1 : 82
S2 : 81
S1 : 80
S2 : 79
……
继续改写,将锁从外部传入。
ThreadDemo6.java
class ThreadDemo6{
public static void main(String[] args){
Object lock = new Object();
Saler s1 = new Saler("S1",lock);
Saler s2 = new Saler("S2",lock);
s1.start();
s2.start();
}
}
//售票员
class Saler extends Thread{
static int tickets = 100 ;
//锁旗标
Object lock ;
private String name ;
public Saler(String name,Object lock){
this.name = name ;
this.lock = lock ;
}
public void run(){
while(true){
int t = getTicket();
if(t == -1){
return ;
}
else{
System.out.println(name + " : " + t);
}
}
}
//取票
public int getTicket(){
synchronized(lock){
int t = tickets ;
try{
//Thread.sleep(50);
}
catch(Exception e){
}
tickets = tickets - 1 ;
return t < 1 ? -1 : t ;
}
}
}
上面使用的都是同步代码块的方法。同步代码块执行期间,线程始终持有对象的监控权,其他线程处于阻塞状态。
//同步代码块
synchronized{
...
}
卖票而言,通常票池和取票的方法可以封装成一个实体。这是一个临界区。
ThreadDemo7.java
class ThreadDemo7{
public static void main(String[] args){
TicketPool pool = new TicketPool();
Saler s1 = new Saler("S-1",pool);
Saler s2 = new Saler("S-2",pool);
Saler s3 = new Saler("S-3",pool);
Saler s4 = new Saler("S-4",pool);
s1.start();
s2.start();
s3.start();
s4.start();
}
}
//售票员
class Saler extends Thread{
private String name ;
private TicketPool pool;
public Saler(String name,TicketPool pool){
this.name = name ;
this.pool = pool ;
}
public void run(){
while(true){
int no = pool.getTicket();
if(no == 0 ){
return ;
}
else{
System.out.println(name + " : " + no);
Thread.yield();
}
}
}
}
//票池
class TicketPool{
private int tickets = 9 ;
public synchronized int getTicket(){
//同步代码块,以票池本身作为锁旗标
/*synchronized(this){*/
int temp = tickets ;
tickets = tickets - 1 ;
return temp > 0 ? temp : 0 ;
//}
}
}
同步方法
如上例,同步代码块可以改写成同步方法。不过建议写作同步代码块。
同步方法是以当前所在对象做锁旗标。
synchronized(this) === 同步方法。
同步静态方法
同步静态方法,使用类作为同步标记。
public static synchronized xxxx(...){
...
}
wait/notify 机制
通过一个没有正确通信的例子,最终报出异常凸显需要通信机制。
ThreadDemo9.java
class ThreadDemo9{
public static void main(String[] args){
//使用java中集合类,List是列表。
java.util.List<Integer> list = new java.util.ArrayList<Integer>();
Productor p = new Productor("生产者",list);
Consumer c = new Consumer("消费者",list);
p.start();
c.start();
}
}
//生产者
class Productor extends Thread{
private String name ;
private java.util.List<Integer> list ;
public Productor(String name ,java.util.List<Integer> list){
this.name = name ;
this.list = list ;
}
public void run(){
int i = 0 ;
while(true){
list.add(new Integer(i ++));
System.out.print("add: " + i + " ");
}
}
}
//消费者
class Consumer extends Thread{
private String name ;
private java.util.List<Integer> list ;
public Consumer(String name ,java.util.List<Integer> list){
this.name = name ;
this.list = list ;
}
public void run(){
while(true){
if(list.size() > 0){
int i = list.remove(0);
try{
Thread.sleep(2000);
}
catch(Exception e){}
System.out.print("remove : " + i + " ");
}
}
}
}
这里,编译后,运行时指定好最大堆空间。
java -Xmx10m ThreadDemo9
经过漫长的等待,会报出这样的错:
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at Productor.run(ThreadDemo9.java:23)
出现这种现象的原因是生成的速度远远大于消费的速度,最终造成堆溢出。
解决这样的问题,线程间需要一种通信机制。
就是等待/唤醒机制。
wait是在Object类中的,由此可知,任何一个对象都可以作为锁旗标。
代码 暂略。
有关对象监控权、同步锁、锁旗标 之间的关系
暂略。