1,为什么需要线程?
作用:提升cpu的利用率,如,早期的dos系统,执行2个命令时( command 1, command 2 ),如果command1【假如是磁盘遍历文件的IO操作】执行的时间比较长,那么command 2必须等待,这种方式就是同步阻塞,
cpu就闲置了,为了提高cpu的利用率,我们就要使用多线程,如果一个任务时间比较长,cpu就暂时挂起他,去执行另外的线程,所以线程一般是异步的。
2,每一个进程至少会有一个线程在运行
public class Test { public static void main(String[] args) { //打印线程的名称 System.out.println( Thread.currentThread().getName() ); } }
输出结果为 "main" ,注意这个main是线程的名字,跟main函数的名字相同而已。
3,在java中实现多线程有2种方式
>继承Thread类
>实现Runnable接口
在run方法中写线程要执行的任务
class MyThread extends Thread{ public void run(){ System.out.println( "MyThread::run" ); } } public class ThreadUse1 { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); System.out.println( "运行结束" ); } }
从运行结果可知,run方法是在之后执行的,虽然start开启线程比 【System.out.println( "运行结束" );】 他早,这说明,CPU在调用线程的时候,是随机的
4,再次验证cpu调用线程的随机性
class MyThreadRand extends Thread{ public void run(){ try{ for ( int i = 0; i < 10; i++ ) { int time = ( int )( Math.random() * 1000 ); Thread.sleep( time ); System.out.println( "MyThread:" + Thread.currentThread().getName() ); } }catch( InterruptedException e ){ e.printStackTrace(); } } } public class RandThread { public static void main(String[] args) { try{ MyThreadRand mt = new MyThreadRand(); mt.setName( "自定义线程" ); mt.start(); for ( int i = 0; i < 10; i++ ) { int time = ( int )( Math.random() * 1000 ); Thread.sleep( time ); System.out.println( "MainThread:" + Thread.currentThread().getName() ); } }catch( InterruptedException e ){ e.printStackTrace(); } } }
从执行结果可知,线程的调度没有什么规律,是随机的, 这里补充一点,start方法作用是通知 “线程规划器”,这个线程已经准备好了,等待调用线程的run方法,就是让系统安排一个时间来调用run方法。如果直接调用run方法,线程就变成同步方式了,必须等待MyThreadRand的run方法执行完成之后,才会执行main函数中的线程
5,start方法的顺序,不代表线程的启动顺序
class MyThreadStart extends Thread{ private int i; public MyThreadStart( int i ) { this.i = i; } public void run(){ System.out.println( i ); } } public class RandThread2 { public static void main(String[] args) { MyThreadStart s1 = new MyThreadStart( 1 ); MyThreadStart s2 = new MyThreadStart( 2 ); MyThreadStart s3 = new MyThreadStart( 3 ); MyThreadStart s4 = new MyThreadStart( 4 ); MyThreadStart s5 = new MyThreadStart( 5 ); MyThreadStart s6 = new MyThreadStart( 6 ); MyThreadStart s7 = new MyThreadStart( 7 ); MyThreadStart s8 = new MyThreadStart( 8 ); MyThreadStart s9 = new MyThreadStart( 9 ); MyThreadStart s10 = new MyThreadStart( 10 ); s1.start(); s2.start(); s3.start(); s4.start(); s5.start(); s6.start(); s7.start(); s8.start(); s9.start(); s10.start(); } }
6,实现Runnable接口
class MyThreadRunnable implements Runnable { public void run(){ System.out.println( Thread.currentThread().getName() ); } } public class ThreadRunnable { public static void main(String[] args) { MyThreadRunnable mt = new MyThreadRunnable(); Thread t = new Thread( mt ); t.setName( "自定义线程1" ); t.start(); } }
那么两种多线程的实现方式,有什么不同呢?
>继承Thread类
>实现Runnable接口
1,使用继承Thread类的方式,多线程之间的数据不共享
class MyThreadShare extends Thread{ private int count = 5; public MyThreadShare( String name ){ this.setName( name ); } public void run(){ while( count-- > 0 ){ System.out.println( Thread.currentThread().getName() + "->" + count ); } } } public class ThreadShare { public static void main(String[] args) { MyThreadShare mt1 = new MyThreadShare( "A" ); MyThreadShare mt2 = new MyThreadShare( "B" ); MyThreadShare mt3 = new MyThreadShare( "C" ); mt1.start(); mt2.start(); mt3.start(); } }
2,而要想实现线程之间的数据共享,我们可以改一下
备注:线程数据共享与不共享,都有对应的场景,比如火车站4个窗口卖票,很显然需要线程共享数据。如:总共用10张票,如果窗口卖了1张,其他窗口就指剩下9张,这才是比较贴近实际的,如果用第一种方式,相当于有40张余票了。
class MyThreadShare2 extends Thread{ private int count = 5; public void run(){ while( count-- > 0 ){ System.out.println( Thread.currentThread().getName() + "->" + count ); } } } public class ThreadShare2 { public static void main(String[] args) { MyThreadShare2 mt = new MyThreadShare2(); Thread ta = new Thread( mt, "A" ); Thread tb = new Thread( mt, "B" ); Thread tc = new Thread( mt, "C" ); ta.start(); tb.start(); tc.start(); } }
从结果上看,好像实现了,数据共享,但是有点异常,B->3 很明显不对,这种现象,在多线程编程里面,叫“线程非安全”。现实生活中也有类似场景,比如4S店卖车,两个客户同时预订了这辆车。那估计少不了一番辩论。怎么解决这个问题呢?一般来说,在客户订车之前,销售员要先查看库存,如果客户下单,要把库存占用。表明有人预订,其他销售员看见了,就知道车被预订了。程序中也是类似。如果要访问这个变量,我们就给他加锁,类似于销售员占用库存。在方法前加上synchronized关键字。那么其他线程访问的时候,必须拿到这把锁,才能访问。synchronized可以在任意对象或者方法上加锁。
class MyThreadShare2 extends Thread{ private int count = 5; // public void run(){ //产生线程非安全问题 synchronized public void run(){ while( count-- > 0 ){ System.out.println( Thread.currentThread().getName() + "->" + count ); } } } public class ThreadShare2 { public static void main(String[] args) { MyThreadShare2 mt = new MyThreadShare2(); Thread ta = new Thread( mt, "A" ); Thread tb = new Thread( mt, "B" ); Thread tc = new Thread( mt, "C" ); ta.start(); tb.start(); tc.start(); } }
3,模拟用户登录场景,如果有两个用户登录,我们让其中一个用户线程占时挂起。看下会出现什么情况
class Login { private static String userName; private static String userPwd; public static void doPost( String _userName, String _userPwd ){ try { userName = _userName; if( userName.equals( "ghostwu" ) ) { Thread.sleep( 3000 ); } userPwd = _userPwd; System.out.println( userName + "---->" + userPwd ); }catch( InterruptedException e ){ e.printStackTrace(); } } } class ThreadA extends Thread{ public void run(){ Login.doPost( "ghostwu", "abc123" ); } } class ThreadB extends Thread{ public void run(){ Login.doPost( "ghostwuB", "abc1234" ); } } public class UserLogin { public static void main(String[] args) { ThreadA ta = new ThreadA(); ThreadB tb = new ThreadB(); ta.start(); tb.start(); } }
在A线程挂起的时候,他之前的赋值已经被B线程改变了,所以结果与预想的ghostwu abc123不同。很明显,我们要上锁。
synchronized public static void doPost( String _userName, String _userPwd ){ try { userName = _userName; if( userName.equals( "ghostwu" ) ) { Thread.sleep( 3000 ); } userPwd = _userPwd; System.out.println( userName + "---->" + userPwd ); }catch( InterruptedException e ){ e.printStackTrace(); } }