与线程有关的知识点总结 java

一、java线程的创建与启动

  1.定义线程

   线程的定义分为两种,一种是继承Java.lang.Thread类,另一种是实现java.lang.Runnable接口。

 2.实例化线程

   定义线程分为两种,则实例化也分为两种。
   1、如果是继承java.lang.Thread类的线程,则直接用关键字new一个对象即可。
   2.如果是实现的java.lang.Runnable接口,则用Thread的构造方法。
     如:Thread(Runnable target) 

 3.启动线程

 调用Thread对象的start()方法,即启动了线程,而不是调用run()或者其它的方法。

 在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真正的线程。

 在调用start()方法之后:发生了一系列复杂的事情

 启动新的执行线程(具有新的调用栈);

 该线程从新状态转移到可运行状态;

 当该线程获得机会执行时,其目标run()方法将运行。

 举例说明

实现Runnable接口的例子:
class DoTest implements Runnable {
    private String name;
    public DoTest(String name) {
        this.name = name;
    } 

    public void run() {
        for (int i = 0; i < 5; i++) {
            for (long k = 0; k < 10000000; k++);
            System.out.println(name + ": " + i);
        } 
    } 
}

public class TestRunnable {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
      DoTest dt1=new DoTest("线程1");
      DoTest dt2=new DoTest("线程2");
      Thread t1=new Thread(dt1);
      Thread t2=new Thread(dt2);
      t1.start();
      t2.start();
	}

}
继承Thread类的例子
public class TestThread extends Thread {
	private String name;
	public static void main(String[] args) {
		// TODO Auto-generated method stub
      Thread t1=new TestThread("线程1");
      Thread t2=new TestThread("线程2");
      t1.start();
      t2.start();
	}
	public TestThread(String name) {
	    this.name = name;
	    } 
	 public void run() {
	    for (int i = 0; i < 5; i++) {
	      for (long k = 0; k < 10000000; k++);
	        System.out.println(name + ": " + i);
	      } 
	    } 
}

二:线程常用的方法

1.获取当前线程的对象的方法是(静态方法):Thread.currentThread();
2.线程睡眠(静态方法)
 Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。
 睡眠的实现代码如下:
 
try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
方法所用的位置:为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。
注意:

1、线程睡眠是帮助所有线程获得运行机会的最好方法。

2、线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。

3sleep()是静态方法,只能控制当前正在运行的线程。

3.线程的优先级设定(非静态方法)
通过对象的setPriority(int newPriority)更改线程的优先级
例如:
public static void main(String[] args) {
		// TODO Auto-generated method stub
      Thread t1=new TestThread("线程1");
      Thread t2=new TestThread("线程2");
      t1.setPriority(4);
      t1.start();
      t2.start();
	}

线程默认优先级是5,范围是1-10,Thread类中有三个常量,定义线程优先级范围:

static int MAX_PRIORITY 
          线程可以具有的最高优先级。
static int MIN_PRIORITY 
          线程可以具有的最低优先级。
static int NORM_PRIORITY 
          分配给线程的默认优先级。

4.线程让步(静态方法)
Thread.yield()方法:暂停当前正在执行的线程对象,并执行其他线程
注意:
yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。yield()并没有让线程进入睡眠或者阻塞状态。
5.join()方法(非静态方法)
Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。其join()有两种不同的重载方法:join()和join(毫秒)。例如:t.join(5000);则让线程(是当前的线程,而不是t线程)等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态。
举例说明:
public class TestThread implements Runnable{
	
	public static int a = 0;

	public void run() {
		for (int k = 0; k < 5; k++) {
			a = a + 1;
		}
	}

	public static void main(String[] args) throws Exception {
		Runnable r = new TestThread();
		Thread t = new Thread(r);
		t.start();	
		System.out.println(a);
	}		
}
这个结果是不确定的,根据不同的机器。原因是这个程序存在两个线程,一个是主线程main,一个是线程t。当主线程执行完t.start()的时候,则会继续往下执行。而这时的线程t也许还在分配资源之类的工作,而a已经输出啦!所以结果不是5.
要想让结果一直为5,则如下:
public class TestThread implements Runnable{
	
	public static int a = 0;

	public void run() {
		for (int k = 0; k < 5; k++) {
			a = a + 1;
		}
	}

	public static void main(String[] args) throws Exception {
		Runnable r = new TestThread();
		Thread t = new Thread(r);
		t.start();	
		t.join();//1.加入join()方法,表示在线程t未结束之前,线程main()不能执行
		t.join(1000);//2.表示线程main得等待线程t执行1000ms之后,线程main才能继续执行,否则一直等待
		System.out.println(a);
	}		
}

三 线程的同步与锁

1.volatile关键词
用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。
public class TestVolatile {
    public volatile static int count = 0;
    public static void inc() {
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
        count++;
    }
    public static void main(String[] args) {
        //同时启动1000个线程,去进行i++计算,看看实际结果
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                	TestVolatile.inc();
                }
            }).start();
        }
        //这里每次运行的值都有可能不同,可能不为1000
        System.out.println("运行结果:Counter.count=" + TestVolatile.count);
    }
}
注意:其结果不是1000。原因是Volatile并不能阻止线程之间发生并发性。
2.synchronized关键字

方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)

所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。(锁是针对的对象,而不是方法),所以这个synchronized分为两种:

1)在某个对象实例内(针对的是实例方法和synchronized(类名.class)这样的代码块)

2)在某个类内(针对的是非实例方法和synchronized(this)这样的代码块)

各位看官,不要着急,我们来具体解释一下(以前我总认为是针对的方法,但是遇到多线程并发的时候,这个理解解释不了一些事情)

 锁定对象实例:
 一个经典的存款实例,Mary的账号是一个对象实例,现在我们模拟1000个线程同时对这个想程存钱,取钱。每个线程都对这个账户存100,取100,按理论来说,其最后结果应该是Mary账号的钱不变,但是发生了改变。
实例代码:
class Account {
    String name;
    float amount;
    
    
    public Account(String name, float amount) {
        this.name = name;
        this.amount = amount;
    }

    public  void deposit(float amt) {
        float tmp = amount;
        tmp += amt;
        
        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }
        
        amount = tmp;
    }

    public  void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }        

        amount = tmp;
    }

    public float getBalance() {
        return amount;
    }
}



public class AccountTest{
    private static int NUM_OF_THREAD = 1000;
    static Thread[] threads = new Thread[NUM_OF_THREAD];
    
    public static void main(String[] args){
        final Account acc = new Account("John", 1000.0f);
        for (int i = 0; i< NUM_OF_THREAD; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                        acc.deposit(100.0f);
                        acc.withdraw(100.0f);
                }
            });
            threads[i].start();
        }

        for (int i=0; i

这时候的结果是
Finally, John's balance is:5900.0。
多线程访问一个对象的方法造成了这种错误。现在我们加上synchronized 这个关键字,用来修饰存钱和取钱两种方法。
class Account {
    String name;
    float amount;
    
    
    public Account(String name, float amount) {
        this.name = name;
        this.amount = amount;
    }

    public  synchronized void deposit(float amt) {
        float tmp = amount;
        tmp += amt;
        
        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }
        
        amount = tmp;
    }

    public  synchronized void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }        

        amount = tmp;
    }

    public float getBalance() {
        return amount;
    }
}



public class AccountTest{
    private static int NUM_OF_THREAD = 1000;
    static Thread[] threads = new Thread[NUM_OF_THREAD];
    
    public static void main(String[] args){
        final Account acc = new Account("John", 1000.0f);
        for (int i = 0; i< NUM_OF_THREAD; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                        acc.deposit(100.0f);
                        acc.withdraw(100.0f);
                }
            });
            threads[i].start();
        }

        for (int i=0; i
Finally, John's balance is:1000.0
其结果为1000。
分析:很明显这1000个线程都是访问的John这个对象。所以这属于对象实例锁的范围。synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。
  锁定类:
现在我们模拟两个对象,对这个账户进行访问,当然现在我们把Acoount这个属性设置为静态变量,属于类的变量,以便让两个对象都可以对它进行访问。
class Account {
    String name;
    static float amount;
    
    
    public Account(String name) {
        this.name = name;
    }

    public   synchronized void deposit(float amt) {
        float tmp = amount;
        tmp += amt;
        
        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }
        
        amount = tmp;
    }

    public  synchronized void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }        

        amount = tmp;
    }

    public float getBalance() {
        return amount;
    }
}



public class AccountTest{
    private static int NUM_OF_THREAD = 1000;
    static Thread[] threads = new Thread[NUM_OF_THREAD];
    static int flag=0;
    public static void main(String[] args){
    	Account.amount=1000;
        final Account acc1 = new Account("John");
        final Account acc2 = new Account("John2");
        for (int i = 0; i< NUM_OF_THREAD; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                	if(flag==0)
                	{
                        acc1.deposit(100.0f);
                        acc1.withdraw(100.0f);
                        flag=1;
                	}
                	else{
                		
                		acc2.deposit(100.0f);
                        acc2.withdraw(100.0f);
                        flag=0;
                	}
                }
            });
            threads[i].start();
        }

        for (int i=0; i
其结果为:

Finally, John's balance is:14100.0。
分析:
account属性虽然为静态变量,属于类的范畴,但是两个方法仍然是对象实例的范畴。acc1对象的线程1正在访问deposit方法,则acc1对象的其它线程是不可能再访问方法的,都会因为synchronized关键字而阻塞。但是这跟acc2对象的线程没有任何关系,acc2的线程是可以访问deposit方法的,所以就造成了上述的并发现象。说明每个对象是一把锁,其只关心自己的锁。所以synchronized修饰非静态方法是属于对象实例锁。
 
现在我们把这个方法都设置为静态的,让其属于类。
class Account {
    String name;
    static float amount;
    
    
    public Account(String name) {
        this.name = name;
    }

    public   synchronized static void deposit(float amt) {
        float tmp = amount;
        tmp += amt;
        
        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }
        
        amount = tmp;
    }

    public  synchronized static void withdraw(float amt) {
        float tmp = amount;
        tmp -= amt;

        try {
            Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等
        } catch (InterruptedException e) {
            // ignore
        }        

        amount = tmp;
    }

    public float getBalance() {
        return amount;
    }
}



public class AccountTest{
    private static int NUM_OF_THREAD = 1000;
    static Thread[] threads = new Thread[NUM_OF_THREAD];
    static int flag=0;
    public static void main(String[] args){
    	Account.amount=1000;
        final Account acc1 = new Account("John");
        final Account acc2 = new Account("John2");
        for (int i = 0; i< NUM_OF_THREAD; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                	if(flag==0)
                	{
                        acc1.deposit(100.0f);
                        acc1.withdraw(100.0f);
                        flag=1;
                	}
                	else{
                		
                		acc2.deposit(100.0f);
                        acc2.withdraw(100.0f);
                        flag=0;
                	}
                }
            });
            threads[i].start();
        }

        for (int i=0; i

其结果为:
Finally, John's balance is:1000.0
 
分析:现在方法属于类方法,所有的对象,所有操作这个对象的线程都共享类这一个锁。所以锁的作用才会生效。
synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

上述都是针对方法来说的,我们经常见到的是对代码块上锁,synchronized(this或者类){}。其实也属于对象实例或者类。总结如下:

 

 (1)当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

 (2)然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

 (3)尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

 (4)第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

 (5)以上规则对其它对象锁同样适用.

四、线程之间的相互唤醒(本节完全参考http://blog.csdn.net/zyplus/article/details/6672775这个博客,写的比较不错)

  上节讲的是synchronized()线程之间的同步互斥操作,接下来要说的是线程之间的相互唤醒wait()、notify()操作。 Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

  单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

public class MyThreadPrinter2 implements Runnable {   
  
    private String name;   
    private Object prev;   
    private Object self;   
  
    private MyThreadPrinter2(String name, Object prev, Object self) {   
        this.name = name;   
        this.prev = prev;   
        this.self = self;   
    }   
  
    @Override  
    public void run() {   
        int count = 10;   
        while (count > 0) {   
            synchronized (prev) {   
                synchronized (self) {   
                    System.out.print(name);   
                    count--;  
                    
                    self.notify();   
                }   
                try {   
                    prev.wait();   
                } catch (InterruptedException e) {   
                    e.printStackTrace();   
                }   
            }   
  
        }   
    }   
  
    public static void main(String[] args) throws Exception {   
        Object a = new Object();   
        Object b = new Object();   
        Object c = new Object();   
        MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);   
        MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);   
        MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);   
           
           
        new Thread(pa).start();
        new Thread(pb).start();
        new Thread(pc).start();    }   
}  
先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。具体来说就是,在main主线程启动ThreadA后,需要在ThreadA执行完,在prev.wait()等待时,再切回线程启动ThreadB,ThreadB执行完,在prev.wait()等待时,再切回主线程,启动ThreadC,只有JVM按照这个线程运行顺序执行,才能保证输出的结果是正确的。而这依赖于JVM的具体实现。考虑一种情况,如下:如果主线程在启动A后,执行A,过程中又切回主线程,启动了ThreadB,ThreadC,之后,由于A线程尚未释放self.notify,也就是B需要在synchronized(prev)处等待,而这时C却调用synchronized(prev)获取了对b的对象锁。这样,在A调用完后,同时ThreadB获取了prev也就是a的对象锁,ThreadC的执行条件就已经满足了,会打印C,之后释放c,及b的对象锁,这时ThreadB具备了运行条件,会打印B,也就是循环变成了ACBACB了。
   解决上述情况,只需要在每个线程启动之后,让主线程main休眠一段时间,以便让上述线程完全执行完run()方法!
  new Thread(pa).start();
        Thread.sleep(100);
        new Thread(pb).start();
        Thread.sleep(100);
        new Thread(pc).start();  

你可能感兴趣的:(JAVA笔试分析)