今年大三下学期了,学校开了一门《多核多线程》课程,老师给我们主要讲解的就是Java的线程方面的知识,上午老师讲完课程了,觉得蛮精髓就想写一篇博客记录下来,免得忘记。闲话不多说了,下面来看看:
首先来引进一个例子:程序的输出是5吗?
创建一个Javaproject,创建包,创建一个名称为Demo1的Class,代码如下:
“`
public class Demo1 extends Thread{
public static int a = 0;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
a = a + 1;
}
}
public static void main(String[] args) throws Exception {
Runnable r = new Demo1();
Thread t = new Thread(r);
t.start();
System.out.println(a);
}
}
控制台输出的信息是:0。运行过程:当主线程main方法运行System.out.println(a)的时候,子线程还没有真正的运行,或许正在分配资源准备运行。当执行t.start()之后,接着执行输出语句System.out.println(a),这个时候得到的a的值还没有改变。那么如何才能让输出结果为5呢?可以在t.start()后面加一行代码,t.join().方法join的作用是:等待调用该方法的线程终止。
上面这个例子的变量a就是一个共享变量,主线程main可以调用,子线程也可以调用。这也就引发出许多问题:两个或者两个以上的线程同时对一个变量进行操作,就可能发生两个线程改变同一个值的问题。下面再来一个稍微麻烦点的例子:
同步访问
创建一个对象类IntGenerator,没有什么实质性的用处
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
public void cancel(){
canceled = true;
}
public boolean isCanceled(){
return canceled;
}
}
一个EvenGenerator类,主要问题就出现在这里,这个类其实就是一个偶数生成器,num的初始值为0,自加两次,那么返回的值必定也是一个偶数,而事实区不一定。
public class EvenGenerator extends IntGenerator{
private int num = 0;
@Override
public int next(){
num++;
num++;
return num;
}
}
接着再定义一个AccessThread类继承自Thread:
public class AccessThread extends Thread{
private int id;
private EvenGenerator gen;
public AccessThread(int id,EvenGenerator gen)
{
super();
this.id = id;
this.gen = gen;
}
public long getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public EvenGenerator getGen() {
return gen;
}
public void setGen(EvenGenerator gen) {
this.gen = gen;
}
@Override
public void run() {
while (!gen.isCanceled()) {
int num = gen.next();
if (num%2!=0) {
// 如果num不是一个偶数的话,输出信息,并且打印num的值,退出循环,程序终止。
System.out.println("出错" + id + ":" + num);
gen.cancel();
}
}
}
}
接着来一个测试类:EvenTest:
public class EvenTest {
public static void main(String[] args) {
// 共享对象
EvenGenerator gen = new EvenGenerator();
ExecutorService exe = Executors.newCachedThreadPool();//开启一个线程池
// 创建100个线程同时对Gen对象进行操作,都执行next()方法
for (int i = 0; i < 100; i++) {
exe.execute(new AccessThread(i, gen));
}
exe.shutdown();
}
}
程序运行可以看到输出信息:出错**,说明偶数生成器生产出来奇数了,问题出现了。假设这100个线程中的线程1和线程2运行过程为:
问题就出现在红色框框里面,这个时候num的值为3,恰好这个时候进行判断num%2,得到的结果就会和预想的不一样了,那么怎么更改才能得到正确的呢?只需要在EvenGenerator类的next方法改为:public synchronized int next()就可以避免这种错误了。(synchronized的意思为同步的)
并发访问
下面还有一个更简洁的例子:
创建一个类SharedObject:
public class SharedObject {
private int val = 0;
public synchronized int getVal() {
return val;
}
public synchronized void setVal(int val) {
this.val = val;
}
}
一个AccessThread继承自Thread:
public class AccessThread extends Thread {
private SharedObject o;
public AccessThread(SharedObject o) {
this.o = o;
}
@Override
public void run() {
synchronized (o)
{
for (int i = 0; i < 1000; i++) {
o.setVal(o.getVal() + 1);
o.setVal(o.getVal() - 1);
}
}
}
}
测试类:
public class SharedTest {
public static void main(String[] args) {
ExecutorService exe = Executors.newCachedThreadPool();
SharedObject o = new SharedObject();
for (int i = 0; i < 100; i++) {
exe.execute(new AccessThread(o));
}
System.out.println(o.getVal());
exe.shutdown();
}
}
这个例子和上面一个差不多,但是更加简洁一些,重点在SharedObject 的get()和set()方法上面,添加了一个synchronized,并且AccessThread类的run()方法中,synchronized (o)就是相当于把当前的对象给锁住了,即:当前线程在对val进行更改,不允许其他线程使用,也就是互斥了。如果去掉这几个synchronized ,那么程序输出的val就不会是0了,并且get(),set()方法前的synchronized 也不可以删除,此处是为了防止其他进程(不是AccessThread里面的线程)来访问。
个人总结
:
同步:多个线程可以同时对一个对象进行操作(如:多个人对一个银行账号进行操作)
互斥:多个线程同时对一个对象进行操作。A进行操作的时候,B不能进行操作,B进行操作的时候,A不能进行操作。