在java的学习过程中,不可避免的要接触到多线程的学习,下面是我的一写学习笔记,如有不妥的地方,还望各路大神不吝指教!!!
首先,我们简单说明一下进程和线程,进程简单直译就是正在进行中的程序,在进程中一个负责程序执行的控制单元(也可以理解为执行路径)就是线程。一个进程中可以有多条执行路径,即多路径,至少要有一个线程。开启多个线程,为了共同运行多个代码,每个线程都有自己的运行内容,这个内容就是线程要执行的任务。如果要调用多线程,一般有两种方法:
一、继承Thread类,其中必须要覆盖一个方法函数run(),然后创建线程对象,最后调用start()方法开启线程,start()方法会自动调用run()方法来执行这个线程的任务。
举个例子:
class Test extends Thread{
private String name;
public Test(String name){
this.name = name;
}
public void run(){
for(int i=0; i<10; i++){
System.out.println(name+"run..."+i);
}
}
public static void main(String[] args){
Test t1 = new Test("a");
Test t2 = new Test("b");
t1.start();
t2.start();
}
}
我们会从执行结果中发现,a与b交替执行,这是因为线程a和线程b在互相争夺cpu资源交替执行。另外,我们还会发现在调用的start()方法,实际上是调用run()方法。那么我们为什么不直接去调用run()方法呢?线程的运行需要本地操作系统的支持才可以,查看start()源码会发现:
start()方法调用了start0()的方法,这个方法用了native关键字,这个关键字表示调用本地操作系统的函数,这就是为什么多线程的实现需要本地操作系统的支持。值得注意的是start()方法重复调用的话,会出现java.lang.IllegalThreadStateException异常(昨天我的同学还问我,线程在杀死后还能再次调用start开启吗,我天真地说为什么不呢,然后就呵呵了)。
二、实现Runnable方法实现多线程,当一个类已经是子类的时候,由于java不支持多继承,所以此时不能让该类再继承Thread类,只能用到Runnable接口。首先让一个类实现Runnable接口,然后覆盖接口中的run()方法,将线程的任务代码封装到run方法中,接着通过Thread类创建线程对象,并将Runnable接口的类的对象作为Thread类的构造函数的参数进行传递,最后调用线程对象的start()方法开启线程。
举个例子:
public class Zi extends Fu implements Runnable{
public void run(){
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName()+"run..."+i);
}
}
}
public class Fu{
public void show(){
System.out.println("Fu is showing...");
}
}
public class Main(){
public static void main(String[] args){
Fu fu = new Fu();
Zi zi = new Zi();
Thread t1 = new Thread(zi);
Thread t2 = new Thread(zi);
t1.start();
t2.start();
}
}
实现Runnable方法有两个好处:1.将线程的任务从Thread的子类中抽离出来,进行单独封装,按照面向对象的思想将任务封装成对象。2.有效的避免了java中单继承的局限性。3.实现Runnable接口适合多个相同的程序代码的线程去处理同一个资源,所以,一般创建多线程的时候,我们经常使用Runnable接口去实现。
值得注意的:main方法也是一个线程,在java中,每次程序至少启动两个线程,一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,就是在操作系统中启动了一个进程。在java中的所有的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到cpu的资源了。另外,主线程可以在子线程结束之前结束,并且子线程不受其影响,不会因为主线程的结束而结束。
最后,连带的说一下线程的其他方法,join()强制执行线程的方法,sleep(2000)线程休眠的方法(参数是休眠的时间按毫秒计算),interrupt()中断线程的方法。
在java程序中,只要前台有一个线程在运行,那么整个java程序进程就不会消失,只有将线程设置为后台线程,这样即使java进程消失了,次后台线程依然能够继续运行(这个又让我想去昨天我同学跟我提到过的线程池的问题,因为线程的每次开启和杀死都是很耗费资源的,我们通过建立线程池可以有效的避免这种资源消耗的过程,线程池中的线程一般都是死循环,不会被杀死,这样就可以保证每次调用线程的时候,避免start()使用完后也不必kill()掉。)
class Test implements Runnable{
public void run(){
while(true){
Systtem.out.println(Thread.currentThread().getName()+"running...");
}
}
public static void main(){
Test test = new Test();
Thread demo = new Thread(test,"线程");
demo.setDaemon(true);
demo.start();
}
}
多线程可能会出现安全问题,然而出现这些安全问题的条件是:1.多个线程在操作共享数据。2.操作共享数据的线程代码有多条。
这就会产生一个问题:当一个线程操作共享数据时,其他线程也参与了运算,就会导致线程安全问题的产生。当然解决这一类问题的思路:1.将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候其他线程是不可以参与运算的,必须要当前线程把这些代码执行完毕,其他线程才可以参与运算。解决方法可以用同步代码块来解决,其格式如下:(同步可以理解为在某一时间段中只有一个线程在运行,其他线程必须等到这个线程结束之后才能继续运行)
synchronized(对象){
//需要同步的代码
}
一般都是把当前对象this当作是同步对象,举个例子(买票的经典例子):
class Ticket implements Runnable{
private int count = 5;
public void run(){
for(int i=0; i<10; i++){
synchronized(this){
if(count>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在售票:"+this.count--);
}
}
}
}
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t, "一号窗口");
Thread t2 = new Thread(t, "二号窗口");
Thread t3 = new Thread(t, "三号窗口");
t1.start();
t2.start();
t3.start();
}
}
当然也有另一种方法:采用同步方法。
同步方法的格式框架:
synchronized 方法返回类型方法名(参数列表){
// your codes
}
还用上面同样的例子,采用同步方法来实现如下:
class Ticket implements Runnable{
private int count = 5;
public void run(){
for(int i=0; i<10; i++)
sale();
}
public synchronized void sale(){
if(count > 0){
try{
Thread.sleep(1000);
}catch(InterruptedExcetption e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在售票"+count);
}
}
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t, "窗口1");
Thread t2 = new Thread(t, "窗口2");
Thread t3 = new Thread(t, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
这里需要注意的是,当多个线程共享一个资源的时候需要进行同步,但是过多的同步有可能导致死锁问题。如何解决死锁的问题呢,,,我们回头再说,这个应该挺复杂的,该睡觉了