一、 了解进程和线程
在多任务系统中,每个独立执行的程序称为进程,也就是“正在进行的程序”。我们现在使用的操作系统一般都是多任务的,既能够同时执行多个应用程序,实际情况是,操作系统负责对cpu等设备的资源进行分配和管理,虽然这些设备某一时刻只能做一件事,但以非常小的时间间隔交替执行多个程序,就可以给人以同时执行多个程序的感觉。
一个进程中又可以包含一个或多个线程,一个线程就是一个程序内部的一条执行线索,如果一程序中实现多段代码同时交替运行,就需要产生多个线程,并指定每个线程上所要运行的程序代码段,这就是多线程
1.程与单线程的对比:
比如以下这段代码是单线程的,执行的结果是run():main,它只会执行run()方法,当run方法执行完后才会执行main后面的方法
class ThreadDemo1 { public static void main(String [] args) { new TestThread().run(); while(true) { //打印当前线程的名称 System.out.println("main():"+Thread.currentThread().getName()); } } } class TestThread { public void run() { while(true) { System.out.println("run():"+Thread.currentThread().getName()); } } }以下这段代码是多线程的,执行的结果是run():Thread-0;和mainThread-0交替执行,实现多线程首先要继承Thread这个类,覆盖run方法,然后new这个子类调用它的start方法就可以创建出多线程。
class ThreadDemo1 { public static void main(String [] args) { new TestThread().start(); while(true) { System.out.println("main():"+Thread.currentThread().getName()); } } } class TestThread extends Thread { public void run() { while(true) { System.out.println("run():"+Thread.currentThread().getName()); } } }二、 用Thread类创建线程
要讲一段代码在一个新的线程上运行,该代码应该在一个类的run函数中,并且run函数所在的类是Thread类的子类。倒过来看,我们要实现多线程,必须编写一个继承Thread类的子类,子类要覆盖Thread类中的run函数,在子类的run函数中调用想在新线程上运行的程序代码。
启动一个新的线程,我们不是直接调用Thread的子类对象的run方法,而是调用Thread的子类对象的start(从Thread类继承到的)方法,Thread类对象的的start方法将产生 一个新的线程,并在该线程上运行该Thread类对象中的run方法,根据面向对象运行时的多态性,在该线程上实际运行的是Thread子类对象中的run方法
由于线程的代码段在run方法中,那么该方法执行完成以后,线程也就向应得结束了,因而我们可以通过控制run方法中循环条件来控制线程的结束。
三、 后台线程与联合线程
如果我们对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程。反之如果我们没有调用setDaemon或setDaemon传递为false,则这个线程为前台线程。
1.线程和后台线程的区别:
class ThreadDemo1 { public static void main(String [] args) { new TestThread().start(); /*while(true) { System.out.println("main():"+Thread.currentThread().getName()); }*/ } } class TestThread extends Thread { public void run() { while(true) { System.out.println("run():"+Thread.currentThread().getName()); } } }
这是一个前台线程,主线程结束了,而java线程并没有结束,对java程序来说只要还有一个前台线程在运行,整个进程都不会结束
这是一个后台线程,这段程序说明了只有后台线程在运行,没有前台线程了,那么整个java进程很快就会结束
也就是说对java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程在运行,这个进程就会结束
我们如何调用线程对象的join()方法,将一个线程合并到另外一个线程中去:
class ThreadDemo1 { public static void main(String [] args) { Thread tt =new TestThread(); tt.start(); int index=0; while(true) { if(index++==1000) try {tt.join();} catch (InterruptedException e) {} System.out.println("main():"+Thread.currentThread().getName()); } } } class TestThread extends Thread { public void run() { while(true) { System.out.println("run():"+Thread.currentThread().getName()); } } }以上示例代码是将两个线程同时开启,当while循环1000次以后两个线程就会合并成一个线程来执行,那我们如何再恢复两个进程分开执行呢,我们只需要将tt.join(10000);方法中传递时间值就可以了,表示合并10秒后再分开执行两个进程 。
四、使用Runnable接口创建多线程
Runnable接口创建多线程示例:
class ThreadDemo1 { public static void main(String [] args) { Thread tt =new Thread(new TestThread()); tt.start(); int index=0; while(true) { if(index++==100) try {tt.join();} catch (InterruptedException e) {} System.out.println("main():"+Thread.currentThread().getName()); } } } class TestThread implements Runnable { public void run() { while(true) { System.out.println("run():"+Thread.currentThread().getName()); } } }实现runnable接口同样也可以创建多线程,它和继承Thread两者有什么区别呢,假设我们要模拟卖票,有四个窗口共同卖100张票,那我们就要同时创建出四个线程来卖100张票,那我们使用继承Thread方式来模拟的话,它会将每个线程各创建100张票,这就不符合我们的要求了,而用实现接口这种方式的话,就可以实现上述要求,所以说实现runnable接口创建多线程会更加的灵活。以下是实现runnable接口来模拟卖票:
class ThreadDemo1 { public static void main(String [] args) { TestThread tt =new TestThread(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); } } class TestThread implements Runnable { int tickets=100; public void run() { while(true) { if(tickets>0) System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } }Runnable接口创建多线程适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效的分离,较好的体现了面向对象的设计思想,可以避免由于java单继承特性带来的局限性。我们经常碰到这样一种情况,既当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个只能采用实现Runable
当线程被构造时,需要代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runable接口的类的实例
事实上,几乎所有多线程应用都可用Runable接口方式
五、多线程在实际中的应用
网络聊天程序的收发:
1)如果一方从键盘上读取了数据并发给了对方,程序运行到“读取对方回送的数据”如果对方没有回应,就一直等待对方回送的数据,程序不能再做其他任何事情,这时程序处于阻塞状态,即使用户想正常终止程序运行都不可能,更不能实现再给对方发送一条消息,催促对方赶快应答这件事情。
2)如果程序没有事先从键盘上读取数据并向外发送,程序将一直在"从键盘上读取数据"处阻塞,即使有数据从网上发过来,程序无法到达"读取对方回送的数据处"程序将不能收到别处 先主动发过来的数据。
如果是多线程,我们可以将发送和接收分为两个线程来处理,如下图所示,其中线程2为接收线程,线程1为发送线程,即使用户在正在从键盘上读取数据的时候,也可以读取到对方发过来的数据,不会受到键盘输入的约束,互不影响
六、多线程的同步
1.线程安全问题
对上述买票多线程实例会发生意外,这个意外就是,同一个票号有可能打印出多次,有可能也打印出负数零,这就不服和我们的要求了,为了能够直观的显示出来,把以上代码if语句块修改为:
if(tickets>0){ try { Thread.sleep(10); } catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); }这样就可以看问题所在。要解决这个问题就是不能有个两个或以上的线程在if代码块中执行,这就需要使用同步语句块来执行。
2.同步语句块解决安全问题
将上述代码修改为:
class TestThread implements Runnable { int tickets=100; String str=new String(""); public void run() { while(true) { synchronized (str) { if(tickets>0){ try { Thread.sleep(10); } catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } } } }加上同步代码块即可解决,synchronized 要跟一个对象。执行原理是:当后边这个对象的标志位为1时,就会有一个线程去执行这段代码,当有一个线程去执行时,那个对象的标志位就会由1变为0,再来一个线程去执行发现标志位变成0了,此时就会发生阻塞,等待标志位变为1时就会再次执行,这就保证了线程的同步。对象的标志位也叫锁旗标。线程的同步语句是以牺牲程序的性能为代价的,所以代码如果不存在线程安全的问题时,尽量不要使用同步代码块。
3.同步函数
使用同步函数也可以解决线程不安全的问题,在方法前面使用synchronized,如:
class TestThread implements Runnable { int tickets=100; String str=new String(""); public void run() { while(true) { sale(); } } public synchronized void sale(){ if(tickets>0){ try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } }4.代码块与函数间同步
要想代码块要和函数同步的话,他们必须共用一个对象的标志位,那同步函数函数它用的是哪个对象呢,先来看一下以下代码:
class ThreadDemo1 { public static void main(String [] args) { TestThread tt =new TestThread(); new Thread(tt).start(); try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();} tt.str=new String ("method"); new Thread(tt).start(); } } class TestThread implements Runnable { int tickets=100; String str=new String(""); public void run() { if(str.equals("method")){ while(true) { sale(); } }else{ while(true) { synchronized (str) { if(tickets>0){ try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();} System.out.print("sqlle():"); System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } } } } public synchronized void sale(){ if(tickets>0){ try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } }运行的结果是不同步的,也就是说明他们共用不同的对象标志位,实验表明函数用的对象是this,如果我们将代码块改为this对象就可以实现同步,修改为:
public void run() { if(str.equals("method")){ while(true) { sale(); } }else{ while(true) { synchronized (this) { if(tickets>0){ try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();} System.out.print("sqlle():"); System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--); } } } } }七、线程间的通信
有两个缓冲分别用来存放姓名和性别,默认存放的是张三、男,生产者用来放入数据,消费者用来取数据,当生产者放入李四缓冲中的时候,性别还没有放,这时消费者的另一个线程来取数据的时候,取到的时李四、男,这显然是不合适的,这就产生了线程不同步的问题,正确的取法是,当生产者全部把姓名和性别全部存到缓冲中的时候,才能进行消费操作。
//生产者不断生产数据 class Producer implements Runnable { Q q; public Producer(Q q) { this.q=q; } public void run(){ int i=0; while(true) { if(i==0) { q.name="zhagnsan"; try{Thread.sleep(1);}catch(Exception e){} q.sex="male"; } else { q.name="lisi"; q.sex="female"; } i=(i+1)%2; } } } //消费者取数据 class Consumer implements Runnable{ Q q; public Consumer(Q q) { this.q=q; } public void run() { while(true) { System.out.print(q.name); System.out.println(":"+q.sex); } } } class Q { String name=""; String sex=""; } class ThreadCommunation { public static void main(String [] args) { Q q =new Q(); new Thread(new Producer(q)).start(); new Thread(new Consumer(q)).start(); } }
这段代码就会带来线程问题,取出的数据是zhangsan,female,这里只需要使用同一个对象的同步代码快,虽然两个代码块不在同一个类中,但使用的同一个对象,这样也可实现同步
解决如下:
class Producer implements Runnable { Q q; public Producer(Q q) { this.q=q; } public void run(){ int i=0; while(true) { synchronized (q) { if(i==0) { q.name="zhagnsan"; try{Thread.sleep(1);}catch(Exception e){} q.sex="male"; } else { q.name="lisi"; q.sex="female"; } i=(i+1)%2; } } } } class Consumer implements Runnable{ Q q; public Consumer(Q q) { this.q=q; } public void run() { while(true) { synchronized (q) { System.out.print(q.name); System.out.println(":"+q.sex); } } } } class Q { String name=""; String sex=""; } class ThreadCommunation { public static void main(String [] args) { Q q =new Q(); new Thread(new Producer(q)).start(); new Thread(new Consumer(q)).start(); } }运行后,虽然读和取同步了,但是还会出现取的时候,有可能会取多个,我们如何避免这个问题呢,这就使用到了线程间的通信,关于线程通信有四个方法:
wait:告诉当前线程放弃监视器并进入睡眠状态直到其他线程进入同一监视器并调用notify为止。
notify:唤醒同一个对象监视器中调用wait的第一个线程。
notifyAll:唤醒同一个对象监视器中调用wait的所有线程,具有最高优先级的线程首先被唤醒并执行。
解决方案如下:
这样就可以实现生产一个取一个,不会出现消费者还没有取的情况下生产者就放入新的数据,也不会出现生产者还没有生产新的数据的时候消费者就取原来的数据。