接上篇:
Java多线程核心技术一-基础篇synchronzied同步方法
Java多线程核心技术一-基础篇synchronzied同步语句块
1 String常量池特性与同步问题
JVM具有String常量池的功能,如下示例:
public class Test01 {
public static void main(String[] args) {
String a = "a";
String b = "a";
System.out.println(a == b);
}
}
在把synchronzied(string)同步块与String联合使用时,要注意常量池会带来一些意外。
public class Service {
public static void print(String param){
try {
synchronized (param){
while(true){
System.out.println(Thread.currentThread().getName());
Thread.sleep(500);
if(Thread.currentThread().getName().equals("B")){
break;
}
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
}
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run(){
service.print("A");
}
}
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run(){
service.print("A");
}
}
出现这种情况就是因为String的两个值都是"AA",两个线程是持有相同的锁,造成线程B不能执行。这就是String常量池所带来的问题,所以大多数情况下,同步synchronzied都不使用String作为锁对象,而改用其他的。例如 new Object()实例化一个新的object对象时,它并不放入缓存池中,或者执行new String()创建不同的字符串对象,形成不同的锁。下面把synchronzied代码块的锁的对象改成object。
public class Service1 {
public static void print(Object obj){
try{
synchronized (obj){
while(true){
System.out.println(Thread.currentThread().getName());
Thread.sleep(500);
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA1 extends Thread{
private Service1 service1;
public ThreadA1(Service1 service1) {
this.service1 = service1;
}
@Override
public void run(){
service1.print(new Object());
}
}
public class ThreadB1 extends Thread{
private Service1 service1;
public ThreadB1(Service1 service1) {
this.service1 = service1;
}
@Override
public void run(){
service1.print(new Object());
}
}
public class Run2 {
public static void main(String[] args) {
Service1 service1 = new Service1();
ThreadA1 a = new ThreadA1(service1);
a.setName("A");
a.start();
ThreadB1 b = new ThreadB1(service1);
b.setName("B");
b.start();
}
}
A 和B 交替输出的原因是持有的锁不是同一个。
2 多线程死锁
Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,导致所有的任务都无法继续完成。在多线程技术中,死锁是必须要避免的,因为这会造成线程的“假死”。示例:
public class DealThread implements Runnable{
public String username;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setFlag(String username){
this.username = username;
}
@Override
public void run() {
if(username.equals("a")){
synchronized (lock1){
try {
System.out.println("username = " + username);
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println("按lock1 -> lock2代码顺序执行了");
}
}
}
if(username.equals("b")){
synchronized (lock2){
try {
System.out.println("username = " + username);
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println("按lock2 -> lock1代码顺序执行");
}
}
}
}
}
public class Run1 {
public static void main(String[] args) {
try {
DealThread t1 = new DealThread();
Thread a = new Thread(t1);
t1.setFlag("a");
a.start();
Thread.sleep(100);
t1.setFlag("b");
Thread b = new Thread(t1);
b.start();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
可以使用JDK自带的工具来检测是否有死锁现象,进入jdk/bin目录下,执行jps命令:
看到运行线程Run1的id值是12880,在执行jstack命令,查看结果:
如上图,检测出有死锁 。
死锁是程序设计的bug,在设计程序时就要避免双方互相持有对方的锁,只要互相等待对方释放锁,就有可能出现死锁。
3 内置类与静态内置类
synchronzied关键字的知识点还涉及内置类的使用,先来看一个简单的内置类的测试。
public class PublicClass {
private String username;
private String password;
class PrivateClass{
private String age;
private String address;
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void printPublicProperty(){
System.out.println(username + ";"+password);
}
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class Run1 {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass();
publicClass.setUsername("usernameValue");
publicClass.setPassword("passwordValue");
System.out.println(publicClass.getUsername() + ";" + publicClass.getPassword());
PublicClass.PrivateClass privateClass = publicClass.new PrivateClass();
privateClass.setAge("ageValue");
privateClass.setAddress("addressValue");
System.out.println(privateClass.getAge() + ";" + privateClass.getAddress());
}
}
如果PublicClass.java类和Run.java类不在同一个包中,则需要将PrivateClass内置类声明为public。想要实例化内置类,必须使用如下代码:
PublicClass.PrivateClass privateClass = publicClass.new PrivateClass();
还有一种内置类叫静态内置类。
public class PublicClass1 {
static private String username;
static private String password;
static class PrivateClass{
private String age;
private String address;
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void printPublicProperty(){
System.out.println(username + ";"+password);
}
}
public static String getUsername() {
return username;
}
public static void setUsername(String username) {
PublicClass1.username = username;
}
public static String getPassword() {
return password;
}
public static void setPassword(String password) {
PublicClass1.password = password;
}
}
public class Run2 {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass();
publicClass.setUsername("usernameValue");
publicClass.setPassword("passwordValue");
System.out.println(publicClass.getUsername() + ";" + publicClass.getPassword());
PublicClass1.PrivateClass privateClass = new PublicClass1.PrivateClass();
privateClass.setAge("ageValue");
privateClass.setAddress("addressValue");
System.out.println(privateClass.getAge() + ";" + privateClass.getAddress());
}
}
4 内置类的同步
本节测试的案例是内置类中有两个同步方法,但使用不同的锁,输出的结果也是异步的。
public class Inner1 {
public void method1(){
synchronized ("其他的锁"){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "; i = " +(i + 1));
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
synchronized public void method2(){
for (int i = 11; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "; i = " + (i + 1));
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class Run1 {
public static void main(String[] args) {
final Inner1 inner1 = new Inner1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
inner1.method1();
}
},"A");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
inner1.method2();
}
}, "B");
t1.start();
t2.start();
}
}
由于持有不同的锁,所有输出结果是乱序的。
5 锁对象改变导致异步执行
在把任何数据类型作为同步锁时,需要注意是否有多个线程同时争抢锁对象,如果同时争抢相同的锁对象,则这些线程之间就是同步的;如果分别获得自己的锁,那么这些线程之间就是异步的。通常情况下,一旦持有锁后就不再对锁对象进行更改,因为一旦更改就有可能出现一些错误。
public class MyService {
private String lock = "123";
public void testMethod(){
try {
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "开始时间是:" + System.currentTimeMillis());
lock = "456";
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "结束时间时: " + System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private MyService myService;
public ThreadA(MyService myService) {
this.myService = myService;
}
@Override
public void run(){
myService.testMethod();
}
}
public class ThreadB extends Thread{
private MyService myService;
public ThreadB(MyService myService) {
this.myService = myService;
}
@Override
public void run(){
myService.testMethod();
}
}
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
ThreadB b = new ThreadB(service);
a.setName("A");
b.setName("B");
a.start();
Thread.sleep(50);
b.start();
}
}
因为50毫秒后,B现成取得锁时456.
继续实现,修改Run1.java,去掉代码中的Thread.sleep(50)
public class Run2 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
ThreadB b = new ThreadB(service);
a.setName("A");
b.setName("B");
a.start();
b.start();
}
}
需要注意的是,String类型是不可变的,都是创建新的内存空间来解决存储新的字符。
控制台输出的信息说明A线程和B线程检测到锁对象的值为123,且存储到内存空间X的位置,虽然将锁改成了456,并存储内存空间Y的位置,但结果还是同步的,因为A线程和B线程共同争抢的锁是X空间的123,不是Y空间的456。但是,还是会有很小的概率出现一起输出两个begin的情况,因为A线程将锁的值改成456后,B现成才启动区执行run方法,不存在A和B争抢同一把锁的情况,导致B线程获取的是更改后的锁的值(456),并连续输出两个begin。