------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
一、操作系统中线程和进程的概念
现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
“同时”执行是人的感觉,在线程之间实际上轮换执行。
二、Java中的线程
一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。
一旦创建一个新的线程,就产生一个新的调用栈。
实现线程的方式有两种: 1、继承java.lang.Thread,并重写它的run()方法,将线程的执行主体放入其中。
2、实现java.lang.Runnable接口,实现它的run()方法,并将线程的执行主体放入其中。
对于直接继承Thread的类来说,代码大致框架是:
class 类名 extends Thread{
方法1;
方法2;
…
public void run(){
// other code…
}
属性1;
属性2;
…
}
还是来看一个例子帮助大家理解吧
package 线程的例子;
public class ThreadTest {
public static void main(String[] args) {
CountDemo demo=new CountDemo();
demo.start();
}
}
class CountDemo extends Thread{
int number=10;
public void run() {
while(number>0){
number--;
System.out.print("number: "+number);
}
}
}
通过实现Runnable接口:
大致框架是:
class 类名 implements Runnable{
方法1;
方法2;
…
public void run(){
// other code…
}
属性1;
属性2;
…
}
废话不多说,上例子:
public class ThreadTest {
public static void main(String[] args) {
CountDemo demo = new CountDemo();
Thread t = new Thread(demo);
t.start();
}
}
class CountDemo implements Runnable {
int number = 10;
public void run() {
while (number > 0) {
number--;
System.out.print("number: " + number);
}
}
}
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
三 多线程中常用到的方法
Object类的方法:wait(), notify(), notifyAll()
Thread类的方法:sleep(), yield(), join()
1. Object类的方法:wait(), notify(), notifyAll()
用于协调多线程对共享数据的存取,所以必须在Synchronized语句块内使用。
如果在其他地方调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException异常。
wait()
使当前线程暂停执行并释放对象锁,让其它线程可以进入Synchronized数据块。
当前线程被放入对象等待池中。
notify()
调用notify方法后,将从“对象等待池”中移走任意的线程并放到“锁标志等待池”中。
如果“对象等待池”中没有线程,则notify不起作用。
notifyAll()
从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中
2. Thread类的方法:sleep(), yield(), join()
sleep()
使当前线程暂停执行一段时间,让其它线程有机会继续执行。但是不释放对象锁。
在没有Synchronized保护下,高优先级线程调用sleep后,低优先级线程可以执行。
sleep可以使低优先级的线程得到执行的机会。
yield()
与sleep类似,只是不能由用户指定暂停多长时间,并且yield只能让同优先级的线程有执行的机会。
yield()方法称为“退让”。
yield做了如下操作:
先检测当前是否有相同优先级的线程处于可运行状态。
如果有,则把CPU的占有权交给此线程,否则继续运行原来的线程。
yield只是使当前线程重新回到可执行状态,所以执行yield的线程有可能在进入到可执行状态后马上又被执行。
当一个线程对象调用yield()方法时会马上交出执行权,回到可运行状态,等待OS的再次调用。
join()
当前运行的线程可以调用另一个线程的join()方法,当前运行的线程将转入阻塞状态,直到另一个线程运行完毕,它才进入可运行状态。
说到线程同步,大部分情况下, 我们是在针对“单对象多线程”的情况进行讨论,一般会将其分成两部分,一部分是关于“共享变量”,一部分关于“执行步骤”。
共享变量
当我们在线程对象(Runnable)中定义了全局变量,run方法会修改该变量时,如果有多个线程同时使用该线程对象,那么就会造成全局变量的值被同时修改,造成错误。我们来看下面的代码:
public class ThreadTest {
public static void main(String[] args) {
CountDemo demo = new CountDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();
t2.start();
}
}
class CountDemo implements Runnable {
public int sum = 0;
public void run() {
for (int i = 0; i <= 100; i++) {
sum += i;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + sum);
}
}
输出结果
Thread-010100
Thread-110100
这个示例中,线程用来计算1到100的和是多少,我们知道正确结果是5050(好像是高斯小时候玩过这个?),但是上述程序返回的结果是10100,原因是两个线程同时对sum进行操作。
执行步骤
我们在多个线程运行时,可能需要某些操作合在一起作为“原子操作”,即在这些操作可以看做是“单线程”的,例如我们可能希望输出结果的样子是这样的:
线程1:步骤1
线程1:步骤2
线程1:步骤3
线程2:步骤1
线程2:步骤2
线程2:步骤3
如果同步控制不好,出来的样子可能是这样的:
线程1:步骤1
线程2:步骤1
线程1:步骤2
线程2:步骤2
线程1:步骤3
线程2:步骤3
如何控制线程同步
既然线程同步有上述问题,那么我们应该如何去解决呢?针对不同原因造成的同步问题,我们可以采取不同的策略。
控制共享变量
我们可以采取3种方式来控制共享变量。
将“单对象多线程”修改成“多对象多线程”
上文提及,同步问题一般发生在“单对象多线程”的场景中,那么最简单的处理方式就是将运行模型修改成“多对象多线程”的样子,针对上面示例中的同步问题,修改后的代码如下:
public class ThreadTest {
public static void main(String[] args) {
/*
* CountDemo demo = new CountDemo();
*
* Thread t1 = new Thread(demo); Thread t2 = new Thread(demo);
*/
// 将“单对象多线程”修改成“多对象多线程”
CountDemo demo1 = new CountDemo();
CountDemo demo2 = new CountDemo();
Thread t1 = new Thread(demo1);
Thread t2 = new Thread(demo2);
t1.start();
t2.start();
}
}
输出结果:
Thread-15050
Thread-05050
我们可以看到,上述代码中两个线程使用了两个不同的Runnable实例,它们在运行过程中,就不会去访问同一个全局变量。
将“全局变量”降级为“局部变量”
既然是共享变量造成的问题,那么我们可以将共享变量改为“不共享”,即将其修改为局部变量。这样也可以解决问题,同样针对上面的示例,这种解决方式的代码如下:
public class ThreadTest {
public static void main(String[] args) {
CountDemo demo = new CountDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
// 将“单对象多线程”修改成“多对象多线程”
/*
* CountDemo demo1 = new CountDemo(); CountDemo demo2 = new CountDemo();
* Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2);
*/
t1.start();
t2.start();
}
}
class CountDemo implements Runnable {
public void run() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + sum);
}
我们可以看出,sum变量已经由全局变量变为run方法内部的局部变量了。
控制执行步骤
说到执行步骤,我们可以使用synchronized关键字来解决它。
public class ThreadTest {
public static void main(String[] args) {
CountDemo demo = new CountDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
// 将“单对象多线程”修改成“多对象多线程”
/*
* CountDemo demo1 = new CountDemo(); CountDemo demo2 = new CountDemo();
* Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2);
*/
t1.start();
t2.start();
}
}
class CountDemo implements Runnable {
int sum = 0;
public synchronized void run() {
for (int i = 0; i <= 100; i++) {
sum += i;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + sum);
sum=0;
}
}
在线程同步的话题上,synchronized是一个非常重要的关键字。它的原理和数据库中事务锁的原理类似。
在一个对象中,用synchonized声明的方法为同步方法。Java中有一个同步模型-监视器,负责管理线程对对象中的同步方法的访问,它的原理是:赋予该对象唯一一把'钥匙',当多个线程进入对象,只有取得该对象钥匙的线程才可以访问同步方法,其它线程在该对象中等待,直到该线程用wait()方法放弃这把钥匙,其它等待的线程抢占该钥匙,抢占到钥匙的线程后才可得以执行,而没有取得钥匙的线程仍被阻塞在该对象中等待。
我们在使用过程中,应该尽量缩减synchronized覆盖的范围,原因有二:1)被它覆盖的范围是串行的,效率低;2)容易产生死锁。