浅谈java中的多线程和锁
什么是多线程?
多线程通俗的来说就是同时有多个线程在运行,但是运行是在并发执行,即并行,而单线程就是事情必须按照顺序来执行,只能在执行完一件事情之后才能执行下一件事情。
每当建立一个线程就会有一个线程栈,在线程中运行哪个方法,哪个方法就会入栈,方法执行完之后就会出栈,当线程栈中没有任何要执行的方法的时候就是一个线程执行完毕了。
我们所说的main方法就是一个主线程。
多线程举例
我们先来建一个线程类,这个类继承了Java提供的Thread类
public class ThreadTest extends Thread{
private static int a=0;
private String name;
public ThreadTest(String name){
this.name=name;
}
public void run(){
for (int i=0;i<20;i++){
a++;
System.out.println(this.name+"-----"+a);
}
}
}
然后在主函数中执行
public class Main {
public static void main(String[] args){
ThreadTest t1=new ThreadTest("线程1");
t1.start();
}
}
主函数中只有一个线程在执行,我们来看一下结果:
这个显然是我们想要的结果,接下来我们为main函数加上一句输出
public class Main {
public static void main(String[] args){
ThreadTest t1=new ThreadTest("线程1");
t1.start();
System.out.println("hello world");
}
}
我们的本意是想让线程t1执行完毕之后才输出hello world,但是我们来看一下结果:
可以看到主线程并不会等待t1执行完之后才输出,怎么让他输出我们想要的结果呢?
我们给主函数再加点东西
public class Main {
public static void main(String[] args){
ThreadTest t1=new ThreadTest("线程1");
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world");
}
}
再看执行结果:
那么这个join()方法的功能就出来啦!join()方法可以让之后的代码等待t1执行完之后才执行
我们再对main方法进行修改
public class Main {
public static void main(String[] args){
ThreadTest t1=new ThreadTest("线程1");
t1.start();
try {
t1.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world");
}
}
执行之后就会发现,hello world这句话在t1执行完之后延迟6000ms出现,那么sleep()方法的作用也就有了,sleep()方法可以让线程“沉睡”一段时间之后接着执行
接下来我们再增加一个线程
public class Main {
public static void main(String[] args){
ThreadTest t1=new ThreadTest("线程1");
ThreadTest t2=new ThreadTest("线程2");
t1.start();
t2.start();
}
}
为了方便,我们把ThreadTest类中run()方法里的循环次数改为10
我们来看一下运行结果:
乍一看没有什么问题,就是顺序可能和我们想的不一样,但这是多线程的正常现象。
那如果我们把ThreadTest类改成下面的:
public class ThreadTest extends Thread{
private static int a=0;
private String name;
public ThreadTest(String name){
this.name=name;
}
public void run(){
for (int i=0;i<10;i++){
a++;
System.out.println(this.name+"-----"+a);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们再来看一下执行结果:
我们就会看到重复的输出,这就是我们所说的线程不安全,我们不能让多线程在“沉睡”几秒之后执行结果就不对了,我们要保证在任何情况下,多线程执行的结果都是我们想要的结果,这样才是线程安全的。
那么怎样保证线程安全呢?
实现线程安全的方法—锁
为了不让结果出现上面的情况,我们引入了锁的概念。在上面的例子中,a
是一个共享变量,有可能在一个线程访问它的时候,另一个线程也在访问,若两个线程访问的时候读到的数据都是1,那么写回的时候就会都是2,这就出现了上面输出重复数据的情况,也就是说有线程读取的是“脏数据”。
为了避免上述情况发生,我们写一个类,采用公共对象的方式来实现加锁。
public class DataTest {
private static int a=0;
//对直接修改共享变量的方法进行加锁
public synchronized void add(String name){
if(a==20){
return;
}
if(a<20){
a++;
//name是线程的名字
System.out.println(name+"----"+a);
}
}
}
线程类做一些改变
public class ThreadTest extends Thread{
private String name;
//添加一个公共对象
private DataTest d;
public ThreadTest(String name,DataTest d){
this.name=name;
this.d=d;
}
public void run(){
for(int i=0;i<10;i++){
//调用公共对象d的add方法
d.add(name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
main方法如下:
public class Main {
public static void main(String[] args){
//创建一个公共对象
DataTest d=new DataTest();
ThreadTest t1=new ThreadTest("线程1",d);
ThreadTest t2=new ThreadTest("线程2",d);
t1.start();
t2.start();
}
}
我们来看一下执行结果:
可以看到执行结果是正确的,证明我们加的锁起作用了。
这里注意一下:只能对方法加锁,不能对公共变量进行加锁
,但是对方法加锁,锁的不是方法,而是方法里面的共享变量。
加锁要对直接操作共享变量的方法进行加锁,特别是共享资源是引用类型里的一部分时
。
类锁、对象锁
对类中操作同一共享资源
的所有非静态方法
进行加锁,所有操作这一共享资源的非静态方法会按顺序执行,即互相锁定(注意非静态方法操作的是同一个对象,否则会锁不住
),静态方法加锁也是可以互相锁定的。这两个相互锁定都有一个前提条件:必须操作的同一共享资源
。
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
静态方法的锁属于类锁,非静态方法的锁属于对象锁,所以静态锁和非静态锁是互相锁不住的。
类中定义了两个非静态属性,两个非静态方法加锁之后,当其中一个加锁方法访问其中一个属性时,另一个加锁方法就不能再访问另一个属性,因为一个非静态方法加锁之后会锁住整个对象,这就是对象锁的作用。
最后我们要知道:线程加锁会使程序变慢哦!