本文讲解3点:
一. 对比单线程和多线程的区别
二. 创建多线程的2种方式:1. extends Thread 2.implements Runnable
三. 线程同步
一. 对比单线程和多线程的区别
1. 单线程
TestThread.java
public class TestThread {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
new ThreadDemo().run();
while(true){
System.out.println("main():"+Thread.currentThread().getName());
}
}
}
class ThreadDemo{
public void run(){
while(true){
System.out.println("run():"+Thread.currentThread().getName());
}
};
}
运行结果截图:
结果分析:其实这只是一个普通的单线程程序,所以main()方法里 while()的代码不会被执行到。而且,new ThreadDemo().run(); 是运行在main 线程内。
2. 多线程 (TestThread.java 改成一个多线程,对比运行结果)
TestThread2.java
public class TestThread2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
new ThreadDemo2().start();
while(true){
System.out.println("main():"+Thread.currentThread().getName());
}
}
}
class ThreadDemo2 extends Thread{
public void run(){
while(true){
System.out.println("run():"+Thread.currentThread().getName());
}
};
}
运行结果截图:
分析结果: 线程ThreadDemo2 和 线程main 交替执行,无序。
跟上面的TestThread.java对比,2点区别:1. TestThread.java 中ThreadDemo不是线程,因为没有extends Thread 也没有implements Runnable , 所以 TestThread.java 是普通的单线程程序。 而 TestThread中ThreadDemo2是线程,因为extends Thread . 2, ThreadDemo2 的启动线程的方法是start(),不是run().
-----------------------------------------------------------------------------------------------------------
二. 创建多线程的2种方式:1. extends Thread 2.implements Runnable
一). 用Thread类创建线程
1. 要将一段代码在一个新的线程上运行,该代码应该在一个类的run()函数中,并且run()函数所在的类是Thread的子类。倒过来看,我们要实现多线程,必须编写一个继承了Thread类的子类,子类要覆盖Thread类中的run()函数,在子类的run()函数中调用想在想在新线程上运行的程序代码。
2. 启动线程,要start()方法,而不会用run()方法。
3. 由于线程的代码段在run方法中,那么该方法执行完以后线程也就响应结束了,因而可以通过控制run方法中循环的条件来控制线程的结束。
1)前台线程,守护线程(也叫后台线程)和联合线程
1. 如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了守护线程。
2. 对于java程序来说,只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中只有守护线程运行,这个进程就会结束。‘
3. pp.join()的作用是把pp所对应的线程合并到调用pp.join();语句的线程中。
3.1 TestThread3.java
public class TestThread3 {
public static void main(String[] args) {
// TODO Auto-generated method stub
new ThreadDemo3().start();
}
}
class ThreadDemo3 extends Thread{
public void run(){
while(true){
System.out.println("run():"+Thread.currentThread().getName());
}
};
}
运行结构截图:
程序分析:main()方法执行完 new ThreadDemo3().start(); 这条语句后,main方法就就结束了,也就是main方法所在的main线程就结束了。注意:虽然main线程结束了,但是java程序并没有结束,因为前台线程ThreadDemo3 还在执行。此程序说明:java程序中,只要还有一个前台线程在运行,那么java程序都不会结束(虽然main线程已经结束了)。
3.2 TestThread4.java
public class TestThread4 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread tt = new ThreadDemo4();
tt.setDaemon(true);//设置线程为 守护线程
tt.start();
}
}
class ThreadDemo4 extends Thread{
public void run(){
while(true){
System.out.println("run():"+Thread.currentThread().getName());
}
};
}
运行结果截图:
程序结果分析:ThreadDemo4为守护线程,java程序中只有守护线程时,java程序马上结束。
4. TestThread5.java (解析 tt.join() , tt.join(10000) 的意义)
public class TestThread5 {
public static void main(String[] args) {
ThreadDemo5 tt = new ThreadDemo5();
tt.start();
int index = 0 ;
while(true){
if (index ++ == 100) {
try {
<span style="color:#cc0000;">tt.join();</span>
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("main():"+Thread.currentThread().getName());
}
}e.printStackTrace();
}
class ThreadDemo5 extends Thread{
public void run(){
while(true){
System.out.println("run():"+Thread.currentThread().getName());
}
};
}
程序分析:
1. tt.join() . 运行结果是: 刚开始 ThreadDemo5 tt线程 和 main线程交替运行,当main线程的 index 达到100时 , 因为tt.join,则表示 tt线程加入main线程,即tt线程和main线程合并成一个新的单线程。程序运行结果是: 刚开始 thread-0 , main 交替 , 当main线程的 index 达到100时,一直是 thread-0 到最后。 因为 当main线程的 index 达到100时,tt线程已经加入main线程,变成一个单线程,所以执行到tt线程时,要等到tt线程执行完才继续执行main线程。
2. tt.join(10000) . 运行结果是:刚开始 tt线程和main线程交替运行,当main线程的index 达到100时,则tt线程加入main线程成为一个新的单线程(10秒),10秒过后,tt线程和main线程再次各自成为单独的线程,形成刚开始的多线程。即:tt.join(10000) 表示:tt线程加入main线程形成新的单线程10秒钟。
二) . 用 实现Runnable 接口的方式创建线程
class ThreadDemo implements Runnable { public void run(){}};
main(){ Thread tt = new Thread(new ThreadDemo()); tt.start(); }
三). 两种方式创建 线程的区别
eg: 火车站卖票100张,分四个窗口同时卖。比较使用extends Thread 和 implements Runnable 的区别。
对比结果:推荐使用Runnable ,几乎所有要实现多线程的程序,都推荐使用implements Runnable。
1. 使用extends Thread,java程序会默认尽量使用1个thread来卖票。
2. 使用 implements Runnable , java 程序会自动调用4个thread来卖票。
4.1 TestThread6.java
package com.thread;
public class TestThread6 {
public static void main(String[] args) {
new ThreadDemo6().start();
new ThreadDemo6().start();
new ThreadDemo6().start();
new ThreadDemo6().start();
}
}
class ThreadDemo6 /*implements Runnable*/ extends Thread{
int tickets = 100 ;
public void run(){
while(tickets>0){
// if(tickets>0)
{
System.out.println("run():"+Thread.currentThread().getName()+"is saling ticket "+tickets--);
}
}
};
}
运行结果:4个线程分别各卖100张票。
4.2 TestThread7.java
package com.thread;
public class TestThread7 {
public static void main(String[] args) {
ThreadDemo7 t = new ThreadDemo7();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class ThreadDemo7 implements Runnable{
int tickets = 100 ;
public void run(){
while(tickets>0){
// if(tickets>0)
{
System.out.println("run():"+Thread.currentThread().getName()+"is saling ticket "+tickets--);
}
}
};
}
运行结果:
---------------------------------------------------------------------------------------------------------------------------
三. 线程同步
线程同步的方法:使用synchronized 来实现 要确保线程安全的代码块 的原子性操作。
先看一段线程不安全的代码,以卖票100张为例。本例中4个线程卖100张票,则可能会打印出0和负数。正常应该是(100~1).
出现异常的原因是:当线程1执行到 while(tickets>0)这一句时,当时tickets = 1 ,这时操作系统突然切换到线程2(线程1并没有执行system.out.print(tickets--)),线程2此时判断tickets=1,并打印了“卖出票1”,tickets-- 变成了 0 ,这时 操作系统又切换到线程1让它继续执行system.out....(tickets--) ,打印出 "卖出票0" 。 同理,若线程3之前执行到"while(tickets>0)”时当时tickets=2,还没有执行system.out()这一句就被操作系统切换走了,而后来当又切换到线程3执行 system.out.print(tickets--) 时,tickets 可能当时已经变为-1了,所以,线程3 打印“卖出票-1” , 同理,可以退出打印“卖出票-2” 的情况。
分析原因:
因为 while(tickets>0)这一句 没有 和 循环体内的 代码块{system.out.println(tickets--)} 保持同步操作。即应该保持while(tickets>0)和{system.out.println(tickets--)}代码块操作的原子性。
注意:Thread.sleep(10) 这一句只是为了模拟出更容易出现线程不安全的状况,不加这一句,程序也是线程不安全的。
5.1 TestThread8.java
package com.thread;
public class TestThread8 {
public static void main(String[] args) {
ThreadDemo8 t = new ThreadDemo8();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class ThreadDemo8 implements Runnable{
int tickets = 100 ;
public void run(){
while(tickets>0){
/*线程休眠10毫秒,模拟出线程不安全的情况出现。不加sleep()也是线程不安全的,加了sleep()则不安全的结果会更容易出现。*/
try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();};
System.out.println("run():"+Thread.currentThread().getName()+"is saling ticket "+tickets--);
}
}
}
结果截图:
为了确保while(tickets>0)和{system.out.println(tickets--)}代码块操作的原子性,用synchronized(对象)来实现,看下面程序。
5.2 TestThread9.java
package com.thread;
public class TestThread9 {
public static void main(String[] args) {
ThreadDemo9 t = new ThreadDemo9();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class ThreadDemo9 implements Runnable{
int tickets = 100 ;
String str = "";
public void run(){
<span style="color:#ff0000;">synchronized (str)</span> { /* <span style="color:#ff0000;">synchronized (任意对象)可实现代码块的原子性*/</span>
while(tickets>0){
//线程休眠10毫秒,模拟出线程不安全的情况出现。不加sleep()也是线程不安全的,加了sleep()则不安全的结果会更容易出现。
try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();};
System.out.println("run():"+Thread.currentThread().getName()+"is saling ticket "+tickets--);
}
}
}
}
运行结果:
注意 :用synchronized(任一对象名){代码块} ,可实现代码块原子性操作,确保代码块的同步。因为每一个对象都有一个标志位,0或者1.
当str标志位为1时,线程进入代码块,str标志位立刻变为0,变为阻塞状态,其他线程就被阻塞。直到synchronized代码块执行完,str的标志位变为1,才会取消阻塞状态,java程序才能切换到其他线程执行。
str的标志位又称:监视器的锁旗标。
当其他线程访问到str监视器时,若str监视器的锁旗标为0,则其他线程将会进入一个因str监视器锁旗标而产生的等待线程池中。直到str监视器的锁旗标变为1时,才可能享有str监视器的锁旗标。
注意:String str = "" ; 要放在run()方法外。
还可以使用同步函数达到同步效果,看代码:
5.3 TestThread10.java
package com.thread;
public class TestThread10 {
public static void main(String[] args) {
ThreadDemo10 t = new ThreadDemo10();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class ThreadDemo10 implements Runnable{
int tickets = 100 ;
String str = "";
public void run(){
sale();
}
public <span style="color:#ff0000;">synchronized</span> void sale(){
while(tickets>0){
//线程休眠10毫秒,模拟出线程不安全的情况出现。不加sleep()也是线程不安全的,加了sleep()则不安全的结果会更容易出现。
try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();};
System.out.println("run():"+Thread.currentThread().getName()+"is saling ticket "+tickets--);
}
}
}
注意: 在 方法名前加上 synchronized 关键字,则 sale()方法是线程安全的,监视器是 this 对象。
即可使用同步代码块来实现线程之间的同步,也可以使用同步函数来实现线程之间的同步。
怎样使得同步代码块和同步函数保持同步? 使用同一个监视器this即可。看下面的程序。
5.4 TestThread11.java
package com.thread;
public class TestThread11 {
public static void main(String[] args) {
ThreadDemo11 t = new ThreadDemo11();
new Thread(t).start();
try { Thread.sleep(1);} catch (Exception e) { e.printStackTrace();}
t.str = "method";
new Thread(t).start();
}
}
class ThreadDemo11 implements Runnable{
int tickets = 100 ;
String str = "";
public void run(){
if (str.equals("method")) {
sale();
}else {
synchronized (<span style="color:#ff0000;">this</span>) {
while(tickets>0){
//线程休眠10毫秒,模拟出线程不安全的情况出现。不加sleep()也是线程不安全的,加了sleep()则不安全的结果会更容易出现。
try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();};
System.out.println("run():"+Thread.currentThread().getName()+"is saling ticket "+tickets--);
}
}
}
}
public synchronized void sale(){
while(tickets>0){
//线程休眠10毫秒,模拟出线程不安全的情况出现。不加sleep()也是线程不安全的,加了sleep()则不安全的结果会更容易出现。
// try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();};
System.out.print("sale-");
System.out.println("run():"+Thread.currentThread().getName()+"is saling ticket "+tickets--);
}
}
}
注意:让同步代码块 和 同步函数同步 的方法:让他们共用一个监视器。本例中共同的监视器是this对象,不能是str。因为同步函数的监视器是this对象,所以必须是的同步代码块的监视器也为this对象。synchronized (this) 是正确的,若写成 synchronized(str) ,则同步代码块和同步函数不能同步。